Skip to content

Commit e7a271d

Browse files
committed
Add breakdown of fields
1 parent e1d1520 commit e7a271d

1 file changed

Lines changed: 63 additions & 23 deletions

File tree

src/odin/contrib/json_schema/__init__.py

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,40 @@
11
"""JSON schema support for Odin."""
22
import json
3-
from typing import Any, Dict, Final, List, Sequence, TextIO, Type, Union
3+
from typing import Any, Dict, Final, List, Sequence, TextIO, Tuple, Type, Union
44

55
import odin
6+
import odin.validators
67
from odin.registration import get_child_resources
78
from odin.resources import ResourceBase, ResourceOptions
89
from odin.utils import getmeta
910

1011
SCHEMA_DIALECT: Final[str] = "https://json-schema.org/draft/2020-12/schema"
12+
FIELD_SCHEMAS = {
13+
odin.StringField: ("string", {}),
14+
odin.BooleanField: ("boolean", {}),
15+
odin.IntegerField: ("integer", {}),
16+
odin.FloatField: ("number", {}),
17+
odin.ListField: ("array", {}),
18+
odin.DictField: ("object", {}),
19+
odin.DateField: ("string", {"format": "date"}),
20+
odin.TimeField: ("string", {"format": "time"}),
21+
odin.DateTimeField: ("string", {"format": "date-time"}),
22+
odin.EmailField: ("string", {"format": "email"}),
23+
odin.IPv4Field: ("string", {"format": "ipv4"}),
24+
odin.IPv6Field: ("string", {"format": "ipv6"}),
25+
odin.IPv46Field: ("string", {"format": ["ipv4", "ipv6"]}),
26+
odin.PathField: ("string", {}),
27+
odin.RegexField: ("string", {"format": "regex"}),
28+
odin.UrlField: ("string", {"format": "uri"}),
29+
odin.UUIDField: ("string", {"format": "uuid"}),
30+
}
31+
VALIDATOR_SCHEMAS = {
32+
odin.validators.MaxValueValidator: {},
33+
odin.validators.MinValueValidator: {},
34+
odin.validators.LengthValidator: {},
35+
odin.validators.MaxLengthValidator: {},
36+
odin.validators.MinLengthValidator: {},
37+
}
1138

1239

1340
class JSONSchema:
@@ -60,36 +87,52 @@ def _fields_to_properties(self, meta: ResourceOptions) -> Dict[str, Any]:
6087

6188
def _field_to_schema(self, field: odin.Field) -> Dict[str, Any]:
6289
"""Convert a field to a JSON schema."""
63-
6490
if isinstance(field, odin.CompositeField):
65-
return self._composite_field_to_schema(field)
91+
schema = self._composite_field_to_schema(field)
6692

67-
schema = {
68-
"type": self._field_type(field),
69-
"description": field.doc_text or "",
70-
}
93+
else:
94+
type_def, extra_schema = self._field_type(field)
95+
schema = {"type": type_def}
96+
schema.update(extra_schema)
97+
98+
if field.doc_text:
99+
schema["description"] = field.doc_text
71100
if field.choices:
72-
schema["enum"] = tuple(str(value) for value in field.choice_values)
101+
schema.setdefault("enum", field.choice_values)
73102

74103
return schema
75104

76-
def _field_type(self, field: odin.Field) -> Union[str, List[str]]:
105+
def _field_type(
106+
self, field: odin.Field
107+
) -> Tuple[Union[str, List[str]], Dict[str, Any]]:
77108
"""Get the type of a field."""
78109

79-
if isinstance(field, odin.ListField):
110+
field_type = type(field)
111+
if field_type in FIELD_SCHEMAS:
112+
type_name, schema = FIELD_SCHEMAS[field_type]
113+
114+
elif isinstance(field, odin.EnumField):
115+
type_name = "string"
116+
schema = {"enum": tuple(str(item.value) for item in field.enum_type)}
117+
118+
elif isinstance(field, odin.TypedListField):
80119
type_name = "array"
81-
elif isinstance(field, odin.IntegerField):
82-
type_name = "integer"
83-
elif isinstance(field, odin.FloatField):
84-
type_name = "number"
85-
elif isinstance(field, odin.BooleanField):
86-
type_name = "boolean"
87-
elif isinstance(field, odin.DictField):
120+
schema = {"items": self._field_to_schema(field.field)}
121+
122+
elif isinstance(field, odin.TypedDictField):
88123
type_name = "object"
124+
schema = {"additionalProperties": self._field_to_schema(field.value_field)}
125+
89126
else:
90-
type_name = "string"
127+
for field_type, field_info in FIELD_SCHEMAS.items():
128+
if isinstance(field, field_type):
129+
type_name, schema = field_info
130+
break
131+
132+
else:
133+
raise ValueError(f"Unknown field type: {field_type}")
91134

92-
return [type_name, "null"] if field.null else type_name
135+
return ([type_name, "null"] if field.null else type_name), schema
93136

94137
def _composite_field_to_schema(self, field: odin.CompositeField) -> Dict[str, Any]:
95138
"""Convert a composite field to a JSON schema."""
@@ -106,10 +149,7 @@ def _composite_field_to_schema(self, field: odin.CompositeField) -> Dict[str, An
106149
else:
107150
schema = self._schema_def(field.of)
108151

109-
if isinstance(field, odin.DictAs):
110-
pass
111-
112-
elif isinstance(field, odin.ListOf):
152+
if isinstance(field, odin.ListOf):
113153
schema = {"type": "array", "items": schema}
114154

115155
elif isinstance(field, odin.DictOf):

0 commit comments

Comments
 (0)