Skip to content

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 required may be absent or null, whatever its type. This is pycharter's rule, not plain JSON Schema's: it matches ODCS, where required: false means the value may be null, and tabular data, where an empty cell reaches the validator as null. A required field accepts null only when its type includes it (type: [string, "null"]). A plain JSON Schema validator given the same schema rejects those optional nulls.
  • pattern matches anywhere in the value, as in every JSON Schema implementation: pattern: "abc" accepts "xabcx". Write ^...$ to match the whole value. The matches_regex rule matches the same way.
  • uniqueItems compares items by JSON value: objects by their members, 1 equals 1.0, and true does not equal 1.
  • A field with no type accepts any JSON value, as in JSON Schema.
  • anyOf / oneOf of one type plus null (for example oneOf: [{type: number, minimum: 0}, {type: "null"}]) is treated as type: [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 an integer field is graded as 5) unless strict_types is on.
  • With strict_types: true, numeric enum and const fields must satisfy both their declared type and literal membership. An integer with enum: [1] accepts 1 and rejects true, 1.0, and "1". A number still accepts integer and floating-point JSON numbers; its Python result follows the native numeric type. Literal membership distinguishes booleans from numbers, while numeric 1 and 1.0 compare by value. Nullable fields must explicitly permit null in 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.
  • format is an annotation, not an assertion, except that the model checks date, date-time, email and uuid on string fields.
  • For object fields mapped to Python date or datetime, pattern, minLength and maxLength check 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 uses Z, and fractional seconds may be normalized. The returned value stays typed. These assertions do not enable coercion; under strict_types, a string still needs the declared coerce_to_date / coerce_to_datetime. Scalar array items retain their existing residual checks on serialized output. For a timezone requirement, accept both Z and numeric offsets, for example pattern: "(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 (coercion and validations) 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)