zarr_metadata.typed_json
zarr_metadata.typed_json ¶
JSON checked against a TypedDict: the public door.
Every Zarr document and configuration this package describes is a
TypedDict -- ZarrV3ArrayMetadataJSON, GzipCodecConfiguration -- and
check makes any of them executable. It type-checks a JSON value
against the TypedDict and gives back a value of it, or None, and every
problem, each located in the value:
import json
from zarr_metadata import ZarrV3ArrayMetadataJSON
from zarr_metadata.typed_json import check
document, problems = check(json.loads(raw), ZarrV3ArrayMetadataJSON)
for problem in problems:
print(problem.loc, problem.kind, problem.message)
A TypedDict reads as the typing spec defines it, however its module writes annotations:
- A key is required when its
RequiredorNotRequiredqualifier says so, and otherwise when thetotalof the class that declared it does. The runtime's__required_keys__cannot see a qualifier written as a string, asfrom __future__ import annotationswrites every one;typeddict_keysreads the annotations evaluated, and can. - A key the TypedDict does not declare is what
closed,extra_itemsor, when the class says neither, its bases make it: in a closed TypedDict it is reported, asunknown_key, and left out, and the value still comes back; withextra_items=it is checked as that type; in an open one it is kept. - Each annotation is evaluated in the module of the class that wrote
it, as the spec has it, where
get_type_hintswould read what a subclass inherited in the subclass's module. Required,NotRequiredandReadOnlyare peeled wherever they are written; aNewTypereads as the type it names, a type alias as the type it stands for, and a TypedDict or alias that holds itself as deep as the value goes.- A number's type may carry bounds, in annotated-types' vocabulary as
pydantic reads it:
Gt,Ge,Lt,LeandInterval, at any depth, sotuple[Annotated[int, Ge(1)], ...]bounds each element. A value out of them is a problem,invalid_value, whose message says what the type admits and whosectxholds the bounds.Annotatedmay also carry a note, a string or aDoc; any other metadata is aTypeError, since a constraintcheckdoes not read would be one it does not hold a value to. - A union of TypedDicts that each require a key as a
Literalof values of their own is read by the branch that key names, and its problems are that branch's. Otherwise a value is read by the first branch it fits with no problem -- among TypedDicts, the one declaring the most of its keys -- and failing that, reported by the branch with the fewest problems.
What comes back is a value of the TypedDict: arrays as tuples, and each
object a new dict of the keys its type admits. Problems are values, not
exceptions: ValidationProblem(loc, message, kind), with kind one of
missing_key, invalid_type, invalid_value, unknown_key and
invalid_json, so a caller that tolerates a key the TypedDict does not
declare can tell it from a wrong value. Each carries its message's data,
as pydantic's errors and zod's issues do: input, the JSON the value
holds at loc, and ctx, what was expected there -- a type's bounds, or
the values of a Literal.
json_schema writes what check reads as a JSON Schema, draft 2020-12,
for a validator in another language, or an editor: the JSON Schema of
the values check finds no problem with. A TypedDict is an object of its
keys, closed or not as it says; a bound is JSON Schema's keyword for it,
Interval(ge=0, le=9) a minimum and a maximum; a TypedDict or a type
alias is written once, in $defs, under its name. JSON Schema takes a
number with no fraction, 1.0, for an integer, where check wants 1:
from zarr_metadata.typed_json import json_schema
json_schema(GzipCodecConfiguration)
# {'$schema': 'https://json-schema.org/draft/2020-12/schema',
# 'type': 'object',
# 'properties': {'level': {'type': 'integer', 'minimum': 0, 'maximum': 9}},
# 'required': ['level'], 'additionalProperties': False}
check reads the shapes JSON takes and no others -- int, float for
any number, bool, str, None, JSONValue, a Literal,
tuple[T, ...] and tuple[T1, T2], a union, a TypedDict,
Mapping[str, V], a NewType and a type alias -- and a TypedDict
holding anything else is a TypeError naming the member, down to the
TypedDict that holds it. So is one the spec itself refuses, such as a
key both Required and NotRequired, and a generic TypedDict, since no
runtime records what its type arguments bind in its bases' keys. A
string annotation resolves only in its module, so a TypedDict written
inside a function cannot name a class of that function. typing.TypedDict
on Python 3.11 does not record a class's bases, so there a subclass
evaluates what it inherits in its own module; typing_extensions.TypedDict
records them on every version.
JSONSchema
module-attribute
¶
A JSON Schema, as the JSON object it is: arrays as lists, as validators take them.
JSONValue
module-attribute
¶
JSONValue = TypeAliasType(
"JSONValue",
int
| float
| bool
| str
| list["JSONValue"]
| tuple["JSONValue", ...]
| Mapping[str, "JSONValue"]
| None,
)
A recursive type alias for JSON-encodable values.
Defined via TypeAliasType (rather than a plain TypeAlias) so the
self-reference is a named recursion point that pydantic can resolve when
building a TypeAdapter; a bare recursive TypeAlias raises
PydanticUserError/RecursionError at validation time.
Loc
module-attribute
¶
Where in a document a value sits: the keys and indices down to it.
ProblemKind
module-attribute
¶
ProblemKind = Literal[
"missing_key",
"invalid_type",
"invalid_value",
"invalid_json",
"unknown_key",
]
Machine-readable classification of a ValidationProblem.
missing_key: a required key (document key or store key) is absent.invalid_type: a value has the wrong structural type (e.g. a string where a mapping is required, a non-JSON-serializable object).invalid_value: a value has an acceptable type but an invalid content (e.g.zarr_format: 2in a v3 document,order: "Q").invalid_json: bytes that do not decode as JSON.unknown_key: a key an object's type does not declare, where the type says it is closed, as a closed TypedDict does. Whether a Zarr configuration is closed is rarely said (zarr-developers/zarr-specs#270 has been open since 2023), and many readers refuse such a key. It gets a kind of its own so that a caller who tolerates it can tell it from a wrong value, and so that it never masks the other findings about the same object.
__all__
module-attribute
¶
__all__ = [
"JSONSchema",
"JSONValue",
"Loc",
"ProblemKind",
"ValidationProblem",
"check",
"json_schema",
]
ValidationProblem
dataclass
¶
A single problem found in a value: where it is, what is wrong, what kind of wrong, and the data the message is made of.
loc is the path from the root of what was judged to the offending
value, e.g. ("codecs", 0, "name") in a document, and an empty loc
refers to that root.
kind classifies the failure mode for programmatic dispatch; message
is the human-readable description.
input and ctx are what the message says, as data, as pydantic's
ErrorDetails and zod's issues carry theirs. input is the JSON found
at loc -- 12, for a gzip level of 12 -- and UNSET where nothing
is there, as zod has it for a key that is missing (pydantic gives the
object missing it), or where what is there is not JSON, which the
message shows. It is the object the caller handed in, as pydantic's
is, not a copy: a caller that changes its document afterwards changes
what input shows. ctx is what was expected, where that is more
than a type:
gt,ge,ltandle: the bounds the value's type carries, as pydantic names them --{"ge": 0, "le": 9}for a gziplevel, whose type isAnnotated[int, Interval(ge=0, le=9)]-- or a rule says.expected: the values of a closed set, as zod'svaluesholds them, in the order the message lists them -- aLiteral's,node_type's,zarr_format's.
Neither takes part in equality or the repr: a problem is the same
problem when it is found at the same place and says the same thing.
Every function that returns or raises problems fills input from the
value its caller handed it, so a rule says only where a problem is.
Source code in src/zarr_metadata/_json.py
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
ctx
class-attribute
instance-attribute
¶
ctx: Mapping[str, JSONValue] = dataclasses.field(
default_factory=_no_ctx,
kw_only=True,
compare=False,
repr=False,
)
input
class-attribute
instance-attribute
¶
input: JSONValue | UNSET = dataclasses.field(
default=UNSET, kw_only=True, compare=False, repr=False
)
__init__ ¶
__init__(
loc: tuple[str | int, ...],
message: str,
kind: ProblemKind,
*,
input: JSONValue | UNSET = UNSET,
ctx: Mapping[str, JSONValue] = _no_ctx(),
) -> None
__post_init__ ¶
Source code in src/zarr_metadata/_json.py
check ¶
check(
value: object, shape: type[T], loc: Loc = ()
) -> tuple[T | None, tuple[ValidationProblem, ...]]
value type-checked as shape, a TypedDict: a value of it or None, and every problem.
value is refined to JSON first -- arrays as tuples, string keys,
finite floats -- and then checked member by member, each problem
located under loc. What comes back holds what shape admits and
nothing else: a key a closed TypedDict does not declare is reported,
as unknown_key, and left out, and the value still comes back.
Anything else wrong and it does not. TypeError for a shape that is
not a TypedDict, or holds something no parser reads.
Source code in src/zarr_metadata/_typed_json.py
json_schema ¶
json_schema(shape: type) -> JSONSchema
The JSON Schema of the JSON check finds no problem with as shape, a TypedDict.
Draft 2020-12, as a JSON object: arrays as lists, and $schema
first. A TypedDict is an object of its keys, those it requires, and
what any other key may hold -- nothing, in a closed one; a bound is
the keyword JSON Schema has for it, Ge(0) a minimum; a Doc is
the description, which is all that says one, as zod writes only
what .describe() said: a docstring is written for Python's readers;
a Literal is its values; a union is anyOf its branches. A
TypedDict or type alias is written once in $defs, under its name,
and referred to wherever it occurs, but for shape itself, which is
written in place unless it holds itself.
One difference is JSON Schema's own: it takes a number with no
fraction, 1.0, for an integer, where check wants 1. TypeError
for a shape that is not a TypedDict, or holds something no parser
reads, as check raises it.