Type System Notes¶
This page explains the core JSON helper types in jsonpatchx/types.py.
The important idea is simple: JsonPatchX keeps its JSON types true to JSON, not to Python convenience behavior.
JSON Semantics¶
The helper family models JSON values with strict runtime behavior:
- strings stay strings
- booleans stay booleans
- numbers are
intor finitefloat - arrays are concrete
listvalues - objects are concrete
dict[str, ...]values
That means JsonPatchX does not quietly accept Python conveniences that would blur a PATCH contract.
Examples:
"2"does not coerce to2Trueis not treated as a JSON number just because Python makesboola subtype ofintfloat("nan")andfloat("inf")are rejected- tuple-like containers are not accepted as runtime JSON arrays
Those constraints are deliberate. PATCH payloads are usually crossing trust boundaries, so the library prefers explicit rejection over clever coercion.
Helper Types¶
The core helper family is:
JSONBooleanJSONNumberJSONStringJSONNullJSONArray[T]JSONObject[T]JSONScalarJSONContainer[T]JSONValue
JSONScalar is the union of the four scalar helpers.
JSONContainer[T] is the union of JSONArray[T] and JSONObject[T].
JSONValue is the full recursive JSON value type used when a field or API
surface wants an actual JSON document or subdocument.
Type Aliases at Check Time, Pydantic Helpers at Runtime¶
The names in types.py are doing two jobs.
During static type checking, the scalar and container helpers are pleasant type aliases such as:
At runtime, those same names become small Pydantic-aware helper classes with custom validation and JSON Schema behavior.
That split exists because plain aliases were not enough to get all three goals at once:
- strict runtime validation
- readable static types
- stable and minimal OpenAPI output
So when types.py looks more complicated than the JSON domain itself, that is
usually because the implementation is balancing those three requirements on
purpose.
Published Schemas¶
The runtime helper classes are not there only for validation. They also shape the published schema surface.
This matters because some obvious-looking alternatives produce poor OpenAPI:
- helper aliases can become noisy named schema components
- replacing generated schema wholesale can hide useful field keywords
- pushing strictness to the wrong level can make Pydantic accept values that a JSON contract should reject
One design goal here is that constraints layered on top of helper types should survive into the published schema instead of disappearing.
For example, Annotated[JSONNumber, Field(gt=4)] should still advertise that
the value must be greater than 4.
In the current stack, that lower bound is preserved, but not in the desired
OpenAPI form: it is emitted as Pydantic's gt: 4 metadata rather than
normalized OpenAPI exclusiveMinimum: 4.
That is not unique to JSONNumber. Plain Annotated[int | float, Field(gt=4)]
currently behaves the same way under the project's OpenAPI 3.1 output.
JSONValue is the clearest example. At runtime it validates against the full
strict recursive JSON union, but its published JSON Schema is deliberately
inlined as {} so helper internals do not leak into OpenAPI as a named
component.
Contributors should treat that as part of the design, not an implementation accident.
Likewise, JSONBoolean | JSONNull currently renders as:
That is normal for OpenAPI 3.1. In older OpenAPI 3.0 tooling, the same concept
often appeared as nullable: true on a base type instead.
JSONValue and JSONBound¶
JSONValue and JSONBound are related, but they are not interchangeable.
JSONValue is the actual Pydantic-aware JSON value model. Use it when runtime
validation should accept only genuine JSON values with the strict rules above.
JSONBound is a typing bound for generics. It means “JSON-shaped enough to be a
valid type parameter bound,” not “accepted as a runtime JSON payload.”
Examples:
JSONScalaris assignable to bothJSONValueandJSONBoundJSONArray[JSONValue]is assignable to bothJSONValueandJSONBoundJSONArray[JSONScalar]is only assignableJSONBound
If JSONPointer[T] was bound by JSONValue,
JSONPointer[JSONArray[JSONScalar]] would be a type checker error. So T is
bound by JSONBound instead, with the compromise that it's overly permissive.
For example, JSONPointer[tuple[int]] is "type-safe."
For more about why it has to be this way, see Limitations in Python's Type System.
Missing Document Sentinel¶
MISSING is the runtime sentinel for “the document no longer exists.”
Operation Results¶
A custom operation that intends to delete the whole document should return
MISSING, not None. None is JSON null. MISSING means document deletion.
This sentinel exists at the operation-result layer so composed operations can
pass along “the document was deleted” without pretending that state is a JSON
value. ReplaceOp, for example, literally implements root replacement as
RemoveOp followed by AddOp: root removal returns MISSING, and root add
recreates the document from that state.
Validation Boundaries¶
MISSING is not itself treated as a JSON value. The JSON helper family
(JSONBoolean, JSONNumber, JSONString, JSONNull, JSONArray[T],
JSONObject[T], and JSONValue) rejects it during normal Pydantic validation.
So “document missing” is a runtime state that pointer- and selector-based operations handle before type validation, not a value that happens to satisfy a JSON helper type.
JSONPointer Defaults¶
Root-targetable pointer operations handle document absence like this:
- root
getfails because there is no document to read - root
removefails when the document is already missing - root
addrecreates the document - root
removeon an existing document returnsMISSING
JSONSelector Defaults¶
At the root selector $, the default selector behavior mirrors the root
pointer:
- root
getallfails because there is no document to read - root
removeallfails when the document is already missing - root
addallrecreates the document
An individual operation can therefore still delete or recreate the whole
document without implying that MISSING is intrinsically compatible with types
such as JSONBoolean or JSONValue. Higher-level boundaries such as model
revalidation or HTTP response serialization can still reject a final missing
document if that contract requires a real JSON value.