jsonvv/README.md
JSON value validator
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.
config = {
"username": "johndoe",
"email": "[email protected]",
"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!')
Dictionary keys can be specified in several possible ways:
"key": "type""key1,key2,key3": "type""/key\d+/": "type""*required_key": "type"To use regex in a key, wrap it in / ... /.
Syntax:
"/key\d+/": "type"
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 !.
*)Fields marked with * are required. The validation will fail without them.
Syntax:
"*key1": "type"
"*/key\d+/": "type"
$)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.
str)Represents a string value. Optionally, you can specify a regex pattern that the string must match.
Syntax:
"str""str[regex_pattern]"\, and for commas is _.Arguments:
regex_pattern: A regular expression that the string must match. If not specified, any string is accepted.Examples:
Basic string:
"username": "str"
Accepts any string value for the key username.
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.
int)Represents an integer value. You can specify conditions like exact values, ranges, and inequalities.
Syntax:
"int""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:
Basic integer:
"age": "int"
Accepts any integer value for the key age.
Integer with conditions:
"userage": "int[>=0, <=120]"
Accepts integer values between 0 and 120 inclusive.
Specific values and ranges
"rating": "int[1-5]"
"rating": "int[1,2,3,4-5]"
Accepts integer values 1, 2, 3, 4, or 5.
Ranges with negative numbers:
"rating": "int[-100 - -90]"
Accepts integer values from -100 to -90.
double)Represents a floating-point number. Supports the same conditions as integers.
Syntax:
"double""double[conditions]"Arguments:
conditions: A comma-separated list of conditions.Examples:
Basic double:
"price": "double"
Accepts any floating-point number for the key price.
Double with conditions:
"percentage": "double[>=0.0,<=100.0]"
Accepts double values between 0.0 and 100.0 inclusive.
bool)Represents a boolean value (True or False).
Syntax:
"isActive": "bool"
Accepts a boolean value for the key isActive.
array)Represents a list of elements of a specified type. You can specify conditions on the length of the array.
Syntax:
"array[element_type]""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:
Basic array:
"tags": "array[str]"
Accepts a list of strings for the key tags.
Array with length conditions:
"scores": "array[int[>=0,<=100],>=1,<=5]"
Accepts a list of 1 to 5 integers between 0 and 100 inclusive.
Fixed-length array:
"coordinates": "array[double, 2]"
Accepts a list of exactly 2 double values.
More complex restraints:
"coordinates": "array[array[int[>0]] - tuple[1, 1]], 2]"
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:
Basic tuple:
"point": "tuple[int, int]"
Accepts a tuple or list of two integers.
Tuple with mixed types:
"userInfo": "tuple[str, int, bool]"
Accepts a tuple of a string, an integer, and a boolean.
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:
Examples:
Nested dictionary:
"user": {
"name": "str",
"age": "int[>=0]",
"preferences": {
"theme": "str",
"notifications": "bool"
}
}
Defines a nested dictionary structure for the key user.
nil)Represents a None value.
Syntax:
"optionalValue": "int | nil"
Usage:
nil to allow a value to be None.any)Represents any value.
Syntax:
"metadata": "any"
Usage:
any when any value is acceptable.@)Allows you to define reusable types and reference them.
Syntax:
Define a named type:
"@typeName": "type_definition"
Reference a named type:
"key": "@typeName"
Examples:
Defining and using a named type:
"@positiveInt": "int[>0]"
"userId": "@positiveInt"
Defines a reusable type @positiveInt and uses it for the key userId.
-)Allows you to specify that a value should not match a certain type or condition.
Syntax:
"typeA - typeB"
Usage:
typeA but not typeB.Examples:
Excluding certain strings:
"message": "str - str[.*error.*]"
Accepts any string that does not match the regex pattern .*error.*.
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.
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.
Allowing all but a specific value:
"specialNumber": "any - int[0]"
|)Allows you to specify that a value can be one of multiple types.
Syntax:
"typeA | typeB | typeC"
Usage:
Examples:
Multiple possible types:
"data": "int | str | bool"
Accepts an integer, string, or boolean value for the key data.
Combining with arrays:
"mixedList": "array[int | str]"
Accepts a list of integers or strings.
Specifies conditions that values must satisfy, including ranges and specific values.
Syntax:
">value""<value"">=value""<="value""start-end""value1,value2,value3"Examples:
Integer conditions:
"level": "int[>=1,<=10]"
Accepts integers from 1 to 10 inclusive.
Double with range:
"latitude": "double[-90.0 - 90.0]"
Accepts doubles between -90.0 and 90.0 inclusive.
Specific values:
"status": "int[1,2,3]"
Accepts integers that are either 1, 2, or 3.
graph TD
Exception --> JvvException
JvvException --> JvvRuntimeException
JvvException --> JvvSyntaxError
JvvRuntimeException --> UnknownProperty["UnknownProperty
<small>Raised when a key in config
isn't defined in property types</small>"]
JvvRuntimeException --> InvalidPropertyType["InvalidPropertyType
<small>Raised when a value doesn't
match its type definition</small>"]
InvalidPropertyType --> MissingRequiredKey["MissingRequiredKey
<small>Raised when a required key
is missing from config</small>"]
MissingRequiredKey --> MissingGroupKey["MissingGroupKey
<small>Raised when some keys in a
property group are missing</small>"]
JvvSyntaxError --> PropertySyntaxError["PropertySyntaxError
<small>Raised when property type
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;
str: Basic string type.
regex_pattern (optional): A regex pattern the string must match."str[^[A-Za-z]+$]"int: Integer type with conditions.
conditions: Inequalities (>=, <=, >, <), specific values (value1,value2), ranges (start-end)."int[>=0,<=100]"double: Double (floating-point) type with conditions.
int."double[>0.0]"bool: Boolean type.
"bool"array: Array (list) of elements of a specified type.
element_type: Type of elements in the array.length_conditions (optional): Conditions on the array length."array[int[>=0],>=1,<=10]"tuple: Fixed-size sequence of elements of specified types.
"tuple[str, int, bool]"nil: Represents a None value.
"nil"any: Accepts any value.
"any"Type References: Reusable type definitions.
@typeName: Reference to a named type."@positiveInt": "int[>0]""userId": "@positiveInt"Union Types (|): Value must match one of multiple types.
"typeA | typeB""str | int"Subtracting Domains (-): Value must match typeA but not typeB.
"typeA - typeB""int - int[13]" (any integer except 13)!: Escapes commas, slashes, and other jsonvv characters within strings.\: Escapes within a regex pattern.