omegafox/jsonvv
2024-11-27 23:09:25 -06:00
..
jsonvv jsonvv: Allow underscore in reference names 0.2.2 2024-11-27 23:09:25 -06:00
publish.sh jsonvv: Bump to Python 3.8 2024-11-27 18:09:55 -06:00
pyproject.toml jsonvv: Allow underscore in reference names 0.2.2 2024-11-27 23:09:25 -06:00
README.md jsonvv: Allow underscore in reference names 0.2.2 2024-11-27 23:09:25 -06:00

JSONvv

JSON value validator

Overview

This is a simple JSON schema validator library. It was created for Camoufox to validate passed user configurations. Because I found it useful for other projects, I decided to extract it into a separate library.

JSONvv's syntax parser is written in pure Python. It does not rely on any dependencies.

Example

Configuration Validator
config = {
    "username": "johndoe",
    "email": "johndoe@example.com",
    "age": 30,
    "chat": "Hello world!",
    "preferences": {
        "notifications": True,
        "theme": "dark"
    },
    "allowed_commands": [
        "/help", "/time", "/weather"
    ],
    "location": [40.7128, -74.0060],
    "hobbies": [
        {
            "name": "Traveling",
            "cities": ["Paris", "London"]
        },
        {
            "name": "reading",
            "hours": {
                "Sunday": 2,
                "Monday": 3,
            }
        }
    ]
}
validator = {
    "username": "str",  # Basic username
    "email": "str[/\S+@\S+\.\S+/]",  # Validate emails
    "age": "int[>=18]",  # Age must be 18 or older
    "chat": "str | nil",  # Optional chat message
    "preferences": {
        "notifications": "bool",
        "theme": "str[light, dark] | nil",  # Optional theme
    },
    # Commands must start with "/", but not contain "sudo"
    "allowed_commands": "array[str[/^//] - str[/sudo/]]",
    # Validate coordinate ranges
    "location": "tuple[double[-90 - 90], double[-180 - 180]]",
    # Handle an array of hobby types
    "hobbies": "array[@traveling | @other, >=1]",
    "@traveling": {
        # Require 1 or more cities/countries iff name is "Traveling"
        "*name,type": "str[Traveling]",
        "*cities,countries": "array[str[A-Za-z*], >=1]",
    },
    "@other": {
        "name,type": "str - str[Traveling]",  # Non-traveling types
        # If hour(s) is specified, require days have >0 hours
        "/hours?/": {
            "*/day$/": "int[>0]"
        }
    }
}

Then, validate the configuration like this:

from jsonvv import JsonValidator, JvvRuntimeException

val = JsonValidator(validator)
try:
   val.validate(config)
except JvvRuntimeException as exc:
   print("Failed:", exc)
else:
   print('Config is valid!')

Table of Contents


Keys Syntax

Dictionary keys can be specified in several possible ways:

  • "key": "type"
  • "key1,key2,key3": "type"
  • "/key\d+/": "type"
  • "*required_key": "type"

Regex patterns

To use regex in a key, wrap it in / ... /.

Syntax:

"/key\d+/": "type"

Lists of possible values

To specify a list of keys, use a comma-separated string.

Syntax:

"key1,key2,key3": "type"
"/k[ey]{2}1/,key2": "type"

To escape a comma, use !.

Required fields (*)

Fields marked with * are required. The validation will fail without them.

Syntax:

"*key1": "type"
"*/key\d+/": "type"

Grouping keys ($)

Fields that end with $group_name are grouped together. If one of the keys is set, all of the keys in the group must also be set as well.

Syntax:

"isEnabled$group1": "bool"
"value$group1": "int[>0]"

This will require both value is set if and only if isEnabled is set.

Multiple $ can be used to create more complex group dependencies.


Supported Types

String (str)

Represents a string value. Optionally, you can specify a regex pattern that the string must match.

Syntax:

  • Basic string: "str"
  • With regex pattern: "str[regex_pattern]"
  • The escape character for regex is \, and for commas is _.

Arguments:

  • regex_pattern: A regular expression that the string must match. If not specified, any string is accepted.

Examples:

  1. Basic string:

    "username": "str"
    

    Accepts any string value for the key username.

  2. String with regex pattern:

    "fullname": "str[/[A-Z][a-z]+ [A-Z][a-z]+/]"
    

    Accepts a string that matches the pattern of a first and last name starting with uppercase letters.

Integer (int)

Represents an integer value. You can specify conditions like exact values, ranges, and inequalities.

Syntax:

  • Basic integer: "int"
  • With conditions: "int[conditions]"

Arguments:

  • conditions: A comma-separated list of conditions.

Condition Operators:

  • ==: Equal to a specific value.
  • >=: Greater than or equal to a value.
  • <=: Less than or equal to a value.
  • >: Greater than a value.
  • <: Less than a value.
  • range: A range between two values (inclusive).

Examples:

  1. Basic integer:

    "age": "int"
    

    Accepts any integer value for the key age.

  2. Integer with conditions:

    "userage": "int[>=0, <=120]"
    

    Accepts integer values between 0 and 120 inclusive.

  3. Specific values and ranges

    "rating": "int[1-5]"
    "rating": "int[1,2,3,4-5]"
    

    Accepts integer values 1, 2, 3, 4, or 5.

  4. Ranges with negative numbers:

    "rating": "int[-100 - -90]"
    

    Accepts integer values from -100 to -90.

Double (double)

Represents a floating-point number. Supports the same conditions as integers.

Syntax:

  • Basic double: "double"
  • With conditions: "double[conditions]"

Arguments:

  • conditions: A comma-separated list of conditions.

Examples:

  1. Basic double:

    "price": "double"
    

    Accepts any floating-point number for the key price.

  2. Double with conditions:

    "percentage": "double[>=0.0,<=100.0]"
    

    Accepts double values between 0.0 and 100.0 inclusive.

Boolean (bool)

Represents a boolean value (True or False).

Syntax:

"isActive": "bool"

Accepts a boolean value for the key isActive.

Array (array)

Represents a list of elements of a specified type. You can specify conditions on the length of the array.

Syntax:

  • Basic array: "array[element_type]"
  • With length conditions: "array[element_type,length_conditions]"

Arguments:

  • element_type: The type of the elements in the array.
  • length_conditions: Conditions on the array length (same as integer conditions).

Examples:

  1. Basic array:

    "tags": "array[str]"
    

    Accepts a list of strings for the key tags.

  2. Array with length conditions:

    "scores": "array[int[>=0,<=100],>=1,<=5]"
    

    Accepts a list of 1 to 5 integers between 0 and 100 inclusive.

  3. Fixed-length array:

    "coordinates": "array[double, 2]"
    

    Accepts a list of exactly 2 double values.

  4. More complex restraints:

    "coordinates": "array[array[int[>0]] - tuple[1, 1]], 2]"
    

Tuple (tuple)

Represents a fixed-size sequence of elements of specified types.

Syntax:

"tuple[element_type1, element_type2]"

Arguments:

  • element_typeN: The type of the Nth element in the tuple.

Examples:

  1. Basic tuple:

    "point": "tuple[int, int]"
    

    Accepts a tuple or list of two integers.

  2. Tuple with mixed types:

    "userInfo": "tuple[str, int, bool]"
    

    Accepts a tuple of a string, an integer, and a boolean.

Nested Dictionaries

Represents a nested dictionary structure. Dictionaries are defined using Python's dictionary syntax {} in the type definitions.

Syntax:

"settings": {
    "volume": "int[>=0,<=100]",
    "brightness": "int[>=0,<=100]",
    "mode": "str"
}

Usage:

  • Define the expected keys and their types within the dictionary.
  • You can use all the supported types for the values.

Examples:

  1. Nested dictionary:

    "user": {
        "name": "str",
        "age": "int[>=0]",
        "preferences": {
            "theme": "str",
            "notifications": "bool"
        }
    }
    

    Defines a nested dictionary structure for the key user.

Nil (nil)

Represents a None value.

Syntax:

"optionalValue": "int | nil"

Usage:

  • Use nil to allow a value to be None.
  • Often used with union types to specify optional values.

Any (any)

Represents any value.

Syntax:

"metadata": "any"

Usage:

  • Use any when any value is acceptable.
  • Useful for keys where the value is not constrained.

Type References (@)

Allows you to define reusable types and reference them.

Syntax:

  • Define a named type:

    "@typeName": "type_definition"
    
  • Reference a named type:

    "key": "@typeName"
    

Examples:

  1. Defining and using a named type:

    "@positiveInt": "int[>0]"
    "userId": "@positiveInt"
    

    Defines a reusable type @positiveInt and uses it for the key userId.


Advanced Features

Subtracting Domains (-)

Allows you to specify that a value should not match a certain type or condition.

Syntax:

"typeA - typeB"

Usage:

  • The value must match typeA but not typeB.

Examples:

  1. Excluding certain strings:

    "message": "str - str[.*error.*]"
    

    Accepts any string that does not match the regex pattern .*error.*.

  2. Excluding a range of numbers:

    "score": "int[0-100] - int[>=90]"
    

    Accepts integers between 0 and 100, excluding values greater than or equal to 90.

  3. Excluding multiple types:

    "score": "int[>0,<100] - int[>90] - int[<10]"
    # Union, then subtraction:
    "score": "int[>0,<100] - int[>90] | int[<10]"
    "score": "int[>0,<100] - (int[>90] | int[<10])"  # same thing
    # Use parenthesis to run subtraction first
    "score": "int[>0,<50] | (int[<100] - int[<10])"
    "score": "(int[<100] - int[<10]) | int[>0,<50]"
    

    Note: Union is handled before subtraction.

  4. Allowing all but a specific value:

    "specialNumber": "any - int[0]"
    

Union Types (|)

Allows you to specify that a value can be one of multiple types.

Syntax:

"typeA | typeB | typeC"

Usage:

  • The value must match at least one of the specified types.

Examples:

  1. Multiple possible types:

    "data": "int | str | bool"
    

    Accepts an integer, string, or boolean value for the key data.

  2. Combining with arrays:

    "mixedList": "array[int | str]"
    

    Accepts a list of integers or strings.

Conditional Ranges and Values

Specifies conditions that values must satisfy, including ranges and specific values.

Syntax:

  • Greater than: ">value"
  • Less than: "<value"
  • Greater than or equal to: ">=value"
  • Less than or equal to: "<="value"
  • Range: "start-end"
  • Specific values: "value1,value2,value3"

Examples:

  1. Integer conditions:

    "level": "int[>=1,<=10]"
    

    Accepts integers from 1 to 10 inclusive.

  2. Double with range:

    "latitude": "double[-90.0 - 90.0]"
    

    Accepts doubles between -90.0 and 90.0 inclusive.

  3. Specific values:

    "status": "int[1,2,3]"
    

    Accepts integers that are either 1, 2, or 3.


Error Handling

graph TD
    Exception --> JvvException
    JvvException --> JvvRuntimeException
    JvvException --> JvvSyntaxError

    JvvRuntimeException --> UnknownProperty["UnknownProperty<br/><small>Raised when a key in config<br/>isn't defined in property types</small>"]
    JvvRuntimeException --> InvalidPropertyType["InvalidPropertyType<br/><small>Raised when a value doesn't<br/>match its type definition</small>"]
    InvalidPropertyType --> MissingRequiredKey["MissingRequiredKey<br/><small>Raised when a required key<br/>is missing from config</small>"]
    MissingRequiredKey --> MissingGroupKey["MissingGroupKey<br/><small>Raised when some keys in a<br/>property group are missing</small>"]

    JvvSyntaxError --> PropertySyntaxError["PropertySyntaxError<br/><small>Raised when property type<br/>definitions have syntax errors</small>"]

    classDef base fill:#eee,stroke:#333,stroke-width:2px;
    classDef jvv fill:#d4e6f1,stroke:#2874a6,stroke-width:2px;
    classDef runtime fill:#d5f5e3,stroke:#196f3d,stroke-width:2px;
    classDef syntax fill:#fdebd0,stroke:#b9770e,stroke-width:2px;
    classDef error fill:#fadbd8,stroke:#943126,stroke-width:2px;

    class Exception base;
    class JvvException jvv;
    class JvvRuntimeException,JvvSyntaxError runtime;
    class PropertySyntaxError syntax;
    class UnknownProperty,InvalidPropertyType,MissingRequiredKey,MissingGroupKey error;

Types

  • str: Basic string type.

    • Arguments:
      • regex_pattern (optional): A regex pattern the string must match.
    • Example: "str[^[A-Za-z]+$]"
  • int: Integer type with conditions.

    • Arguments:
      • conditions: Inequalities (>=, <=, >, <), specific values (value1,value2), ranges (start-end).
    • Example: "int[>=0,<=100]"
  • double: Double (floating-point) type with conditions.

    • Arguments:
      • Same as int.
    • Example: "double[>0.0]"
  • bool: Boolean type.

    • Arguments: None.
    • Example: "bool"
  • array: Array (list) of elements of a specified type.

    • Arguments:
      • element_type: Type of elements in the array.
      • length_conditions (optional): Conditions on the array length.
    • Example: "array[int[>=0],>=1,<=10]"
  • tuple: Fixed-size sequence of elements of specified types.

    • Arguments:
      • List of element types.
    • Example: "tuple[str, int, bool]"
  • nil: Represents a None value.

    • Arguments: None.
    • Example: "nil"
  • any: Accepts any value.

    • Arguments: None.
    • Example: "any"
  • Type References: Reusable type definitions.

    • Arguments:
      • @typeName: Reference to a named type.
    • Example:
      • Define: "@positiveInt": "int[>0]"
      • Use: "userId": "@positiveInt"

Type Combinations

  • Union Types (|): Value must match one of multiple types.

    • Syntax: "typeA | typeB"
    • Example: "str | int"
  • Subtracting Domains (-): Value must match typeA but not typeB.

    • Syntax: "typeA - typeB"
    • Example: "int - int[13]" (any integer except 13)

Escaping Characters

  • !: Escapes commas, slashes, and other jsonvv characters within strings.
  • \: Escapes within a regex pattern.