11"""JSON schema support for Odin."""
22import 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
55import odin
6+ import odin .validators
67from odin .registration import get_child_resources
78from odin .resources import ResourceBase , ResourceOptions
89from odin .utils import getmeta
910
1011SCHEMA_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
1340class 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