structtype 0.3.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- structtype/__init__.py +17 -0
- structtype/__init__.pyi +350 -0
- structtype/_adapter.py +128 -0
- structtype/_core.cp313-win_amd64.pyd +0 -0
- structtype/_inspect.py +1183 -0
- structtype/_json_schema.py +532 -0
- structtype/_utils.py +410 -0
- structtype/_version.py +24 -0
- structtype/py.typed +0 -0
- structtype-0.3.0.dist-info/METADATA +80 -0
- structtype-0.3.0.dist-info/RECORD +14 -0
- structtype-0.3.0.dist-info/WHEEL +5 -0
- structtype-0.3.0.dist-info/licenses/LICENSE +27 -0
- structtype-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import textwrap
|
|
3
|
+
from collections.abc import Callable, Iterable
|
|
4
|
+
from typing import Any, Final
|
|
5
|
+
|
|
6
|
+
from ._core import NODEFAULT, _dump, _json_encode
|
|
7
|
+
from ._inspect import (
|
|
8
|
+
AnyType,
|
|
9
|
+
BoolType,
|
|
10
|
+
ByteArrayType,
|
|
11
|
+
BytesType,
|
|
12
|
+
CollectionType,
|
|
13
|
+
CustomType,
|
|
14
|
+
DataclassType,
|
|
15
|
+
DateTimeType,
|
|
16
|
+
DateType,
|
|
17
|
+
DecimalType,
|
|
18
|
+
DictType,
|
|
19
|
+
EnumType,
|
|
20
|
+
FloatType,
|
|
21
|
+
FrozenDictType,
|
|
22
|
+
FrozenSetType,
|
|
23
|
+
IntType,
|
|
24
|
+
LiteralType,
|
|
25
|
+
MemoryViewType,
|
|
26
|
+
Metadata,
|
|
27
|
+
NamedTupleType,
|
|
28
|
+
NoneType,
|
|
29
|
+
PydanticType,
|
|
30
|
+
RawType,
|
|
31
|
+
SetType,
|
|
32
|
+
StrType,
|
|
33
|
+
StructType,
|
|
34
|
+
TimeDeltaType,
|
|
35
|
+
TimeType,
|
|
36
|
+
TupleType,
|
|
37
|
+
Type,
|
|
38
|
+
TypedDictType,
|
|
39
|
+
UnionType,
|
|
40
|
+
UUIDType,
|
|
41
|
+
_merge_json,
|
|
42
|
+
_sort_literal_args,
|
|
43
|
+
multi_type_info,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
_REF_TEMPLATE: Final = "#/$defs/{name}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def json_schema(
|
|
50
|
+
type: Any,
|
|
51
|
+
*,
|
|
52
|
+
schema_hook: Callable[[type], dict[str, Any]] | None = None,
|
|
53
|
+
ref_template: str = _REF_TEMPLATE,
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
"""Generate a JSON Schema for a given type.
|
|
56
|
+
|
|
57
|
+
Any schemas for (potentially) shared components are extracted and stored in
|
|
58
|
+
a top-level ``"$defs"`` field.
|
|
59
|
+
|
|
60
|
+
If you want to generate schemas for multiple types, or to have more control
|
|
61
|
+
over the generated schema you may want to use ``json_schema_components`` instead.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
type : type
|
|
66
|
+
The type to generate the schema for.
|
|
67
|
+
schema_hook : callable, optional
|
|
68
|
+
An optional callback to use for generating JSON schemas of custom
|
|
69
|
+
types. Will be called with the custom type, and should return a dict
|
|
70
|
+
representation of the JSON schema for that type.
|
|
71
|
+
ref_template : str, optional
|
|
72
|
+
A template to use when generating ``"$ref"`` fields. This template is
|
|
73
|
+
formatted with the type name as ``template.format(name=name)``. This
|
|
74
|
+
can be useful if you intend to store the ``components`` mapping
|
|
75
|
+
somewhere other than a top-level ``"$defs"`` field. For example, you
|
|
76
|
+
might use ``ref_template="#/components/{name}"`` if generating an
|
|
77
|
+
OpenAPI schema.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
schema : dict
|
|
82
|
+
The generated JSON Schema.
|
|
83
|
+
|
|
84
|
+
See Also
|
|
85
|
+
--------
|
|
86
|
+
json_schema_components
|
|
87
|
+
"""
|
|
88
|
+
(out,), components = json_schema_components(
|
|
89
|
+
(type,),
|
|
90
|
+
schema_hook=schema_hook,
|
|
91
|
+
ref_template=ref_template,
|
|
92
|
+
)
|
|
93
|
+
if components:
|
|
94
|
+
out["$defs"] = components
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def json_schema_dump(
|
|
99
|
+
type: Any,
|
|
100
|
+
*,
|
|
101
|
+
schema_hook: Callable[[type], dict[str, Any]] | None = None,
|
|
102
|
+
ref_template: str = _REF_TEMPLATE,
|
|
103
|
+
) -> bytes:
|
|
104
|
+
"""Generate a JSON Schema for the given type, returned as JSON bytes.
|
|
105
|
+
|
|
106
|
+
This is a convenience wrapper around :func:`json_schema` that encodes the
|
|
107
|
+
result to JSON.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
type : type
|
|
112
|
+
The type to generate the schema for.
|
|
113
|
+
schema_hook : callable, optional
|
|
114
|
+
An optional callback to use for generating JSON schemas of custom
|
|
115
|
+
types.
|
|
116
|
+
ref_template : str, optional
|
|
117
|
+
A template to use when generating ``"$ref"`` fields.
|
|
118
|
+
"""
|
|
119
|
+
return _json_encode(
|
|
120
|
+
json_schema(type=type, schema_hook=schema_hook, ref_template=ref_template)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def json_schema_components(
|
|
125
|
+
types: Iterable[Any],
|
|
126
|
+
*,
|
|
127
|
+
schema_hook: Callable[[type], dict[str, Any]] | None = None,
|
|
128
|
+
ref_template: str = _REF_TEMPLATE,
|
|
129
|
+
) -> tuple[tuple[dict[str, Any], ...], dict[str, Any]]:
|
|
130
|
+
"""Generate JSON Schemas for one or more types.
|
|
131
|
+
|
|
132
|
+
Any schemas for (potentially) shared components are extracted and returned
|
|
133
|
+
in a separate ``components`` dict.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
types : Iterable[type]
|
|
138
|
+
An iterable of one or more types to generate schemas for.
|
|
139
|
+
schema_hook : callable, optional
|
|
140
|
+
An optional callback to use for generating JSON schemas of custom
|
|
141
|
+
types. Will be called with the custom type, and should return a dict
|
|
142
|
+
representation of the JSON schema for that type.
|
|
143
|
+
ref_template : str, optional
|
|
144
|
+
A template to use when generating ``"$ref"`` fields. This template is
|
|
145
|
+
formatted with the type name as ``template.format(name=name)``. This
|
|
146
|
+
can be useful if you intend to store the ``components`` mapping
|
|
147
|
+
somewhere other than a top-level ``"$defs"`` field. For example, you
|
|
148
|
+
might use ``ref_template="#/components/{name}"`` if generating an
|
|
149
|
+
OpenAPI schema.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
schemas : tuple[dict]
|
|
154
|
+
A tuple of JSON Schemas, one for each type in ``types``.
|
|
155
|
+
components : dict
|
|
156
|
+
A mapping of name to schema for any shared components used by
|
|
157
|
+
``schemas``.
|
|
158
|
+
|
|
159
|
+
See Also
|
|
160
|
+
--------
|
|
161
|
+
schema
|
|
162
|
+
"""
|
|
163
|
+
type_infos = multi_type_info(types)
|
|
164
|
+
|
|
165
|
+
component_types = _collect_component_types(type_infos)
|
|
166
|
+
|
|
167
|
+
name_map = _build_name_map(component_types)
|
|
168
|
+
|
|
169
|
+
gen = _SchemaGenerator(name_map, schema_hook, ref_template)
|
|
170
|
+
|
|
171
|
+
schemas = tuple(gen.to_schema(t) for t in type_infos)
|
|
172
|
+
|
|
173
|
+
components = {
|
|
174
|
+
name_map[cls]: gen.to_schema(t, False) for cls, t in component_types.items()
|
|
175
|
+
}
|
|
176
|
+
return schemas, components
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _collect_component_types(type_infos: Iterable[Type]) -> dict[Any, Type]:
|
|
180
|
+
"""Find all types in the type tree that are "nameable" and worthy of being
|
|
181
|
+
extracted out into a shared top-level components mapping.
|
|
182
|
+
|
|
183
|
+
Currently this looks for Struct, Dataclass, NamedTuple, TypedDict, and Enum
|
|
184
|
+
types.
|
|
185
|
+
"""
|
|
186
|
+
components = {}
|
|
187
|
+
|
|
188
|
+
def collect(t):
|
|
189
|
+
if isinstance(
|
|
190
|
+
t, (StructType, TypedDictType, DataclassType, NamedTupleType, PydanticType)
|
|
191
|
+
):
|
|
192
|
+
if t.cls not in components:
|
|
193
|
+
components[t.cls] = t
|
|
194
|
+
for f in t.fields:
|
|
195
|
+
collect(f.type)
|
|
196
|
+
elif isinstance(t, EnumType):
|
|
197
|
+
components[t.cls] = t
|
|
198
|
+
elif isinstance(t, Metadata):
|
|
199
|
+
collect(t.type)
|
|
200
|
+
elif isinstance(t, CollectionType):
|
|
201
|
+
collect(t.item_type)
|
|
202
|
+
elif isinstance(t, TupleType):
|
|
203
|
+
for st in t.item_types:
|
|
204
|
+
collect(st)
|
|
205
|
+
elif isinstance(t, (DictType, FrozenDictType)):
|
|
206
|
+
collect(t.key_type)
|
|
207
|
+
collect(t.value_type)
|
|
208
|
+
elif isinstance(t, UnionType):
|
|
209
|
+
for st in t.types:
|
|
210
|
+
collect(st)
|
|
211
|
+
|
|
212
|
+
for t in type_infos:
|
|
213
|
+
collect(t)
|
|
214
|
+
|
|
215
|
+
return components
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _type_repr(obj):
|
|
219
|
+
return obj.__name__ if isinstance(obj, type) else repr(obj)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _get_class_name(cls: Any) -> str:
|
|
223
|
+
if hasattr(cls, "__origin__"):
|
|
224
|
+
name = cls.__origin__.__name__
|
|
225
|
+
args = ", ".join(_type_repr(a) for a in cls.__args__)
|
|
226
|
+
return f"{name}[{args}]"
|
|
227
|
+
return cls.__name__
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _get_doc(t: Type) -> str:
|
|
231
|
+
assert hasattr(t, "cls")
|
|
232
|
+
cls = getattr(t.cls, "__origin__", t.cls)
|
|
233
|
+
doc = getattr(cls, "__doc__", "")
|
|
234
|
+
if not doc:
|
|
235
|
+
return ""
|
|
236
|
+
doc = textwrap.dedent(doc).strip("\r\n")
|
|
237
|
+
if isinstance(t, EnumType):
|
|
238
|
+
if doc == "An enumeration.":
|
|
239
|
+
return ""
|
|
240
|
+
elif isinstance(t, (NamedTupleType, DataclassType, PydanticType)):
|
|
241
|
+
if doc.startswith(f"{cls.__name__}(") and doc.endswith(")"):
|
|
242
|
+
return ""
|
|
243
|
+
return doc
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _build_name_map(component_types: dict[Any, Type]) -> dict[Any, str]:
|
|
247
|
+
"""A mapping from nameable subcomponents to a generated name.
|
|
248
|
+
|
|
249
|
+
The generated name is usually a normalized version of the class name. In
|
|
250
|
+
the case of conflicts, the name will be expanded to also include the full
|
|
251
|
+
import path.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
def normalize(name):
|
|
255
|
+
return re.sub(r"[^a-zA-Z0-9.\-_]", "_", name)
|
|
256
|
+
|
|
257
|
+
def fullname(cls):
|
|
258
|
+
return normalize(f"{cls.__module__}.{cls.__qualname__}")
|
|
259
|
+
|
|
260
|
+
conflicts = set()
|
|
261
|
+
names: dict[str, Any] = {}
|
|
262
|
+
|
|
263
|
+
for cls in component_types:
|
|
264
|
+
name = normalize(_get_class_name(cls))
|
|
265
|
+
if name in names:
|
|
266
|
+
old = names.pop(name)
|
|
267
|
+
conflicts.add(name)
|
|
268
|
+
names[fullname(old)] = old
|
|
269
|
+
if name in conflicts:
|
|
270
|
+
names[fullname(cls)] = cls
|
|
271
|
+
else:
|
|
272
|
+
names[name] = cls
|
|
273
|
+
return {v: k for k, v in names.items()}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class _SchemaGenerator:
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
name_map: dict[Any, str],
|
|
280
|
+
schema_hook: Callable[[type], dict[str, Any]] | None = None,
|
|
281
|
+
ref_template: str = "#/$defs/{name}",
|
|
282
|
+
):
|
|
283
|
+
self.name_map = name_map
|
|
284
|
+
self.schema_hook = schema_hook
|
|
285
|
+
self.ref_template = ref_template
|
|
286
|
+
|
|
287
|
+
def to_schema(self, t: Type, check_ref: bool = True) -> dict[str, Any]:
|
|
288
|
+
"""Converts a Type to a json-schema."""
|
|
289
|
+
schema: dict[str, Any] = {}
|
|
290
|
+
|
|
291
|
+
while isinstance(t, Metadata):
|
|
292
|
+
schema = _merge_json(schema, t.json_schema_extra)
|
|
293
|
+
t = t.type
|
|
294
|
+
|
|
295
|
+
if check_ref and hasattr(t, "cls"):
|
|
296
|
+
if name := self.name_map.get(t.cls):
|
|
297
|
+
schema["$ref"] = self.ref_template.format(name=name)
|
|
298
|
+
return schema
|
|
299
|
+
|
|
300
|
+
if isinstance(t, (AnyType, RawType)):
|
|
301
|
+
pass
|
|
302
|
+
elif isinstance(t, NoneType):
|
|
303
|
+
schema["type"] = "null"
|
|
304
|
+
elif isinstance(t, BoolType):
|
|
305
|
+
schema["type"] = "boolean"
|
|
306
|
+
elif isinstance(t, (IntType, FloatType)):
|
|
307
|
+
schema["type"] = "integer" if isinstance(t, IntType) else "number"
|
|
308
|
+
if t.ge is not None:
|
|
309
|
+
schema["minimum"] = t.ge
|
|
310
|
+
if t.gt is not None:
|
|
311
|
+
schema["exclusiveMinimum"] = t.gt
|
|
312
|
+
if t.le is not None:
|
|
313
|
+
schema["maximum"] = t.le
|
|
314
|
+
if t.lt is not None:
|
|
315
|
+
schema["exclusiveMaximum"] = t.lt
|
|
316
|
+
if t.multiple_of is not None:
|
|
317
|
+
schema["multipleOf"] = t.multiple_of
|
|
318
|
+
elif isinstance(t, StrType):
|
|
319
|
+
schema["type"] = "string"
|
|
320
|
+
if t.max_length is not None:
|
|
321
|
+
schema["maxLength"] = t.max_length
|
|
322
|
+
if t.min_length is not None:
|
|
323
|
+
schema["minLength"] = t.min_length
|
|
324
|
+
if t.pattern is not None:
|
|
325
|
+
schema["pattern"] = t.pattern
|
|
326
|
+
elif isinstance(t, (BytesType, ByteArrayType, MemoryViewType)):
|
|
327
|
+
schema["type"] = "string"
|
|
328
|
+
schema["contentEncoding"] = "base64"
|
|
329
|
+
if t.max_length is not None:
|
|
330
|
+
schema["maxLength"] = 4 * ((t.max_length + 2) // 3)
|
|
331
|
+
if t.min_length is not None:
|
|
332
|
+
schema["minLength"] = 4 * ((t.min_length + 2) // 3)
|
|
333
|
+
elif isinstance(t, DateTimeType):
|
|
334
|
+
schema["type"] = "string"
|
|
335
|
+
if t.tz is True:
|
|
336
|
+
schema["format"] = "date-time"
|
|
337
|
+
elif isinstance(t, TimeType):
|
|
338
|
+
schema["type"] = "string"
|
|
339
|
+
if t.tz is True:
|
|
340
|
+
schema["format"] = "time"
|
|
341
|
+
elif t.tz is False:
|
|
342
|
+
schema["format"] = "partial-time"
|
|
343
|
+
elif isinstance(t, DateType):
|
|
344
|
+
schema["type"] = "string"
|
|
345
|
+
schema["format"] = "date"
|
|
346
|
+
elif isinstance(t, TimeDeltaType):
|
|
347
|
+
schema["type"] = "string"
|
|
348
|
+
schema["format"] = "duration"
|
|
349
|
+
elif isinstance(t, UUIDType):
|
|
350
|
+
schema["type"] = "string"
|
|
351
|
+
schema["format"] = "uuid"
|
|
352
|
+
elif isinstance(t, DecimalType):
|
|
353
|
+
schema["type"] = "string"
|
|
354
|
+
schema["format"] = "decimal"
|
|
355
|
+
elif isinstance(t, CollectionType):
|
|
356
|
+
schema["type"] = "array"
|
|
357
|
+
if not isinstance(t.item_type, AnyType):
|
|
358
|
+
schema["items"] = self.to_schema(t.item_type)
|
|
359
|
+
if t.max_length is not None:
|
|
360
|
+
schema["maxItems"] = t.max_length
|
|
361
|
+
if t.min_length is not None:
|
|
362
|
+
schema["minItems"] = t.min_length
|
|
363
|
+
if isinstance(t, (SetType, FrozenSetType)):
|
|
364
|
+
schema["uniqueItems"] = True
|
|
365
|
+
elif isinstance(t, TupleType):
|
|
366
|
+
schema["type"] = "array"
|
|
367
|
+
schema["minItems"] = schema["maxItems"] = len(t.item_types)
|
|
368
|
+
if t.item_types:
|
|
369
|
+
schema["prefixItems"] = [self.to_schema(i) for i in t.item_types]
|
|
370
|
+
schema["items"] = False
|
|
371
|
+
elif isinstance(t, (DictType, FrozenDictType)):
|
|
372
|
+
schema["type"] = "object"
|
|
373
|
+
# If there are restrictions on the keys, specify them as propertyNames
|
|
374
|
+
if isinstance(key_type := t.key_type, StrType):
|
|
375
|
+
property_names: dict[str, Any] = {}
|
|
376
|
+
if key_type.min_length is not None:
|
|
377
|
+
property_names["minLength"] = key_type.min_length
|
|
378
|
+
if key_type.max_length is not None:
|
|
379
|
+
property_names["maxLength"] = key_type.max_length
|
|
380
|
+
if key_type.pattern is not None:
|
|
381
|
+
property_names["pattern"] = key_type.pattern
|
|
382
|
+
if property_names:
|
|
383
|
+
schema["propertyNames"] = property_names
|
|
384
|
+
if not isinstance(t.value_type, AnyType):
|
|
385
|
+
schema["additionalProperties"] = self.to_schema(t.value_type)
|
|
386
|
+
if t.max_length is not None:
|
|
387
|
+
schema["maxProperties"] = t.max_length
|
|
388
|
+
if t.min_length is not None:
|
|
389
|
+
schema["minProperties"] = t.min_length
|
|
390
|
+
elif isinstance(t, UnionType):
|
|
391
|
+
structs = {}
|
|
392
|
+
other = []
|
|
393
|
+
none_member = None
|
|
394
|
+
tag_field = None
|
|
395
|
+
for subtype in t.types:
|
|
396
|
+
real_type = subtype
|
|
397
|
+
while isinstance(real_type, Metadata):
|
|
398
|
+
real_type = real_type.type
|
|
399
|
+
if isinstance(real_type, StructType) and not real_type.array_like:
|
|
400
|
+
tag_field = real_type.tag_field
|
|
401
|
+
structs[real_type.tag] = real_type
|
|
402
|
+
elif isinstance(real_type, NoneType):
|
|
403
|
+
none_member = subtype
|
|
404
|
+
else:
|
|
405
|
+
other.append(subtype)
|
|
406
|
+
|
|
407
|
+
options = [self.to_schema(a) for a in other]
|
|
408
|
+
|
|
409
|
+
if len(structs) >= 2:
|
|
410
|
+
mapping = {
|
|
411
|
+
k: self.ref_template.format(name=self.name_map[v.cls])
|
|
412
|
+
for k, v in structs.items()
|
|
413
|
+
}
|
|
414
|
+
struct_schema = {
|
|
415
|
+
"anyOf": [self.to_schema(v) for v in structs.values()],
|
|
416
|
+
"discriminator": {"propertyName": tag_field, "mapping": mapping},
|
|
417
|
+
}
|
|
418
|
+
if options:
|
|
419
|
+
options.append(struct_schema)
|
|
420
|
+
if none_member is not None:
|
|
421
|
+
options.append(self.to_schema(none_member))
|
|
422
|
+
schema["anyOf"] = options
|
|
423
|
+
elif none_member is not None:
|
|
424
|
+
schema["anyOf"] = [struct_schema, self.to_schema(none_member)]
|
|
425
|
+
else:
|
|
426
|
+
schema.update(struct_schema)
|
|
427
|
+
elif len(structs) == 1:
|
|
428
|
+
_, subtype = structs.popitem()
|
|
429
|
+
options.append(self.to_schema(subtype))
|
|
430
|
+
if none_member is not None:
|
|
431
|
+
options.append(self.to_schema(none_member))
|
|
432
|
+
schema["anyOf"] = options
|
|
433
|
+
else:
|
|
434
|
+
if none_member is not None:
|
|
435
|
+
options.append(self.to_schema(none_member))
|
|
436
|
+
schema["anyOf"] = options
|
|
437
|
+
elif isinstance(t, LiteralType):
|
|
438
|
+
# `t.values` may mix types (e.g. `Literal[1, None]`), which a plain
|
|
439
|
+
# `sorted` can't order; reuse the same type-aware sort as
|
|
440
|
+
# `inspect.type_info` so the two entry points agree and neither
|
|
441
|
+
# crashes.
|
|
442
|
+
schema["enum"] = list(_sort_literal_args(t.values))
|
|
443
|
+
elif isinstance(t, EnumType):
|
|
444
|
+
schema.setdefault("title", t.cls.__name__)
|
|
445
|
+
if doc := _get_doc(t):
|
|
446
|
+
schema.setdefault("description", doc)
|
|
447
|
+
schema["enum"] = sorted(e.value for e in t.cls)
|
|
448
|
+
elif isinstance(t, StructType):
|
|
449
|
+
schema.setdefault("title", _get_class_name(t.cls))
|
|
450
|
+
if doc := _get_doc(t):
|
|
451
|
+
schema.setdefault("description", doc)
|
|
452
|
+
required = []
|
|
453
|
+
names = []
|
|
454
|
+
fields = []
|
|
455
|
+
|
|
456
|
+
if t.tag_field is not None:
|
|
457
|
+
required.append(t.tag_field)
|
|
458
|
+
names.append(t.tag_field)
|
|
459
|
+
fields.append({"enum": [t.tag]})
|
|
460
|
+
|
|
461
|
+
for field in t.fields:
|
|
462
|
+
field_schema = self.to_schema(field.type)
|
|
463
|
+
if field.required:
|
|
464
|
+
required.append(field.encode_name)
|
|
465
|
+
elif field.default is not NODEFAULT:
|
|
466
|
+
field_schema["default"] = _dump(field.default, str_keys=True)
|
|
467
|
+
elif field.default_factory in (list, dict, set, bytearray):
|
|
468
|
+
field_schema["default"] = field.default_factory()
|
|
469
|
+
names.append(field.encode_name)
|
|
470
|
+
fields.append(field_schema)
|
|
471
|
+
|
|
472
|
+
if t.array_like:
|
|
473
|
+
n_trailing_defaults = 0
|
|
474
|
+
for n_trailing_defaults, f in enumerate(reversed(t.fields)):
|
|
475
|
+
if f.required:
|
|
476
|
+
break
|
|
477
|
+
schema["type"] = "array"
|
|
478
|
+
schema["prefixItems"] = fields
|
|
479
|
+
schema["minItems"] = len(fields) - n_trailing_defaults
|
|
480
|
+
if t.forbid_unknown_fields:
|
|
481
|
+
schema["maxItems"] = len(fields)
|
|
482
|
+
else:
|
|
483
|
+
schema["type"] = "object"
|
|
484
|
+
schema["properties"] = dict(zip(names, fields))
|
|
485
|
+
schema["required"] = required
|
|
486
|
+
if t.forbid_unknown_fields:
|
|
487
|
+
schema["additionalProperties"] = False
|
|
488
|
+
elif isinstance(
|
|
489
|
+
t, (TypedDictType, DataclassType, NamedTupleType, PydanticType)
|
|
490
|
+
):
|
|
491
|
+
schema.setdefault("title", _get_class_name(t.cls))
|
|
492
|
+
if doc := _get_doc(t):
|
|
493
|
+
schema.setdefault("description", doc)
|
|
494
|
+
names = []
|
|
495
|
+
fields = []
|
|
496
|
+
required = []
|
|
497
|
+
for field in t.fields:
|
|
498
|
+
field_schema = self.to_schema(field.type)
|
|
499
|
+
if field.required:
|
|
500
|
+
required.append(field.encode_name)
|
|
501
|
+
elif field.default is not NODEFAULT:
|
|
502
|
+
field_schema["default"] = _dump(field.default, str_keys=True)
|
|
503
|
+
names.append(field.encode_name)
|
|
504
|
+
fields.append(field_schema)
|
|
505
|
+
if isinstance(t, NamedTupleType):
|
|
506
|
+
schema["type"] = "array"
|
|
507
|
+
schema["prefixItems"] = fields
|
|
508
|
+
schema["minItems"] = len(required)
|
|
509
|
+
schema["maxItems"] = len(fields)
|
|
510
|
+
else:
|
|
511
|
+
schema["type"] = "object"
|
|
512
|
+
schema["properties"] = dict(zip(names, fields))
|
|
513
|
+
schema["required"] = required
|
|
514
|
+
elif isinstance(t, CustomType):
|
|
515
|
+
if self.schema_hook:
|
|
516
|
+
try:
|
|
517
|
+
schema = _merge_json(self.schema_hook(t.cls), schema)
|
|
518
|
+
except NotImplementedError:
|
|
519
|
+
pass
|
|
520
|
+
if not schema:
|
|
521
|
+
raise TypeError(
|
|
522
|
+
"Generating JSON schema for custom types requires either:\n"
|
|
523
|
+
"- specifying a `schema_hook`\n"
|
|
524
|
+
"- annotating the type with `Field(json_schema_extra=...)`\n"
|
|
525
|
+
"\n"
|
|
526
|
+
f"type {t.cls!r} is not supported"
|
|
527
|
+
)
|
|
528
|
+
else:
|
|
529
|
+
# This should be unreachable
|
|
530
|
+
raise TypeError(f"json-schema doesn't support type {t!r}")
|
|
531
|
+
|
|
532
|
+
return schema
|