Built-in coercions and validations¶
PyCharter validates standard JSON Schema and extends it with two charter-specific rule families: coercions (normalise incoming values) and validations (assert business rules). Both are data, not code — they live in the contract and travel with it.
📋 Standard JSON Schema Support¶
A Validator enforces every JSON Schema Draft 2020-12 assertion keyword.
The generated Pydantic model enforces the keywords below directly (the fast
path). Any other assertion — dependentRequired, anyOf / oneOf /
allOf / not, if / then / else, minProperties, propertyNames,
patternProperties, contains, prefixItems, constraints on array items,
and so on — is evaluated after the model by a standards JSON Schema engine,
so no declared rule is skipped. Only contracts that use such keywords pay
for the second pass.
A few details:
- Null and optional fields. A field that is not in
requiredmay be absent ornull, whatever itstype. This is pycharter's rule, not plain JSON Schema's: it matches ODCS, whererequired: falsemeans the value may be null, and tabular data, where an empty cell reaches the validator asnull. A required field acceptsnullonly when its type includes it (type: [string, "null"]). A plain JSON Schema validator given the same schema rejects those optional nulls. patternmatches anywhere in the value, as in every JSON Schema implementation:pattern: "abc"accepts"xabcx". Write^...$to match the whole value. Thematches_regexrule matches the same way.uniqueItemscompares items by JSON value: objects by their members,1equals1.0, andtruedoes not equal1.- A field with no
typeaccepts any JSON value, as in JSON Schema. anyOf/oneOfof one type plusnull(for exampleoneOf: [{type: number, minimum: 0}, {type: "null"}]) is treated astype: [number, "null"], so it gets the same type conversion as a nullable type array.- The engine grades values after the model's type conversion (
"5"in anintegerfield is graded as5) unlessstrict_typesis on. - With
strict_types: true, numericenumandconstfields must satisfy both their declared type and literal membership. Anintegerwithenum: [1]accepts1and rejectstrue,1.0, and"1". Anumberstill accepts integer and floating-point JSON numbers; its Python result follows the native numeric type. Literal membership distinguishes booleans from numbers, while numeric1and1.0compare by value. Nullable fields must explicitly permitnullin their literal assertion. This repairs the earlier strict-mode acceptance of wrong-typed literals. Default lax-mode Literal conversion is unchanged; declared coercion rules still run first. Membership checks both original and converted values, so float rounding cannot admit a different integer. Formatted string literals retain their string representation, including mixed numeric/string enums. Generated JSON Schema retains the authored numeric type and assertions. formatis an annotation, not an assertion, except that the model checksdate,date-time,emailanduuidon string fields.- For object fields mapped to Python
dateordatetime,pattern,minLengthandmaxLengthcheck the supplied string spelling. When a declared coercion has already produced a temporal object, or the caller supplies one, these assertions check its Pydantic JSON representation: UTC usesZ, and fractional seconds may be normalized. The returned value stays typed. These assertions do not enable coercion; understrict_types, a string still needs the declaredcoerce_to_date/coerce_to_datetime. Scalar array items retain their existing residual checks on serialized output. For a timezone requirement, accept bothZand numeric offsets, for examplepattern: "(Z|[+-][0-9]{2}:[0-9]{2})$". - Only local
$refs (#/$defs/...,#/definitions/...) are supported; a contract with any other reference fails to load.
Keywords the model enforces directly:
| Keyword | Type | Description | Example |
|---|---|---|---|
minLength |
string | Minimum string length | {"minLength": 3} |
maxLength |
string | Maximum string length | {"maxLength": 10} |
pattern |
string | Regular expression, matched anywhere in the value | {"pattern": "^[a-z]+$"} |
enum |
any | Allowed values | {"enum": ["a", "b", "c"]} |
const |
any | Single allowed value | {"const": "fixed"} |
minimum |
number | Minimum value (inclusive) | {"minimum": 0} |
maximum |
number | Maximum value (inclusive) | {"maximum": 100} |
exclusiveMinimum |
number | Minimum value (exclusive) | {"exclusiveMinimum": 0} |
exclusiveMaximum |
number | Maximum value (exclusive) | {"exclusiveMaximum": 100} |
multipleOf |
number | Must be multiple of | {"multipleOf": 2} |
minItems |
array | Minimum array length | {"minItems": 1} |
maxItems |
array | Maximum array length | {"maxItems": 10} |
uniqueItems |
array | Array items must be unique | {"uniqueItems": true} |
All schemas are validated against JSON Schema standard before processing, ensuring compliance.
🔧 Built-in Coercions (Charter Extensions)¶
| Coercion | Description |
|---|---|
coerce_to_string |
Convert int, float, bool, datetime, dict, list to string |
coerce_to_integer |
Convert float, string (numeric), bool, datetime to int |
coerce_to_float |
Convert int, string (numeric), bool to float |
coerce_to_boolean |
Convert int, string to bool |
coerce_to_datetime |
Convert string (ISO format), timestamp to datetime |
coerce_to_date |
Convert string (date format), datetime to date (date only, no time) |
coerce_to_uuid |
Convert string to UUID |
coerce_to_lowercase |
Convert string to lowercase |
coerce_to_uppercase |
Convert string to uppercase |
coerce_to_stripped_string |
Strip leading and trailing whitespace from string |
coerce_to_list |
Convert single value to list [value] (preserves None) |
coerce_empty_to_null |
Convert empty strings/lists/dicts to None (useful for nullable fields) |
coerce_to_none |
Always return None (null out a field) |
coerce_to_json |
Parse a JSON string into a dict |
coerce_to_nullable_string |
Convert to string, or None on failure/null |
coerce_to_nullable_integer |
Convert to int, or None on failure/null |
coerce_to_nullable_float |
Convert to float, or None on failure/null |
coerce_to_nullable_boolean |
Convert to bool, or None on failure/null |
coerce_to_nullable_datetime |
Parse datetime, or None on failure/null |
coerce_to_nullable_uuid |
Parse UUID, or None on failure/null |
coerce_to_nullable_json |
Parse JSON, or None on failure/null |
These 21 names are the only coercion vocabulary: the registry
(pycharter.shared.coercions.COERCION_REGISTRY) is the single source of
truth for contract loading, validation, the API, and the UI. An unknown
name fails contract load with a rename hint. Null values are never passed
to coercion functions — nullability is validation's concern; the
coerce_to_nullable_* variants exist to null out unparseable values.
✅ Built-in Validations (Charter Extensions)¶
| Validation | Description | Configuration |
|---|---|---|
min_length |
Minimum length for strings/arrays | {"threshold": N} |
max_length |
Maximum length for strings/arrays | {"threshold": N} |
only_allow |
Only allow specific values | {"allowed_values": [...]} |
greater_than_or_equal_to |
Numeric minimum | {"threshold": N} |
less_than_or_equal_to |
Numeric maximum | {"threshold": N} |
is_positive |
Value must be positive | {"threshold": 0} |
no_capital_characters |
No uppercase letters | null |
no_special_characters |
Only alphanumeric and spaces | null |
non_empty_string |
String must not be empty | null |
matches_regex |
String must contain a match for the regex, like JSON Schema pattern (anchor with ^...$ to match the whole value) |
{"pattern": "..."} |
is_email |
String must be valid email address | null |
is_url |
String must be valid URL | null |
is_alphanumeric |
Only alphanumeric characters (no spaces/special) | null |
is_numeric_string |
String must be numeric (digits, optional decimal) | null |
is_unique |
All items in array must be unique | null |
Note: Charter extensions (
coercionandvalidations) are optional and can be used alongside standard JSON Schema keywords. All validation logic is stored as data in the JSON schema, making it fully data-driven.
🎨 Custom Coercions and Validations¶
Extend Charter with your own coercion and validation functions:
from pycharter.shared.coercions import register_coercion
from pycharter.shared.validations import register_validation
# Register custom coercion
def coerce_to_uppercase(data):
if isinstance(data, str):
return data.upper()
return data
register_coercion("coerce_to_uppercase", coerce_to_uppercase)
# Register custom validation
def must_be_positive(threshold=0):
def _validate(value, info):
if value <= threshold:
raise ValueError(f"Value must be > {threshold}")
return value
return _validate
register_validation("must_be_positive", must_be_positive)