Skip to content

Commit de67486

Browse files
committed
fix: recursive references across multiple schemas
1 parent f0c00b1 commit de67486

File tree

4 files changed

+109
-67
lines changed

4 files changed

+109
-67
lines changed

jsf/parser.py

+59-37
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ def __parse_primitive(self, name: str, path: str, schema: Dict[str, Any]) -> Pri
124124
}
125125
)
126126

127-
def __parse_object(self, name: str, path: str, schema: Dict[str, Any]) -> Object:
127+
def __parse_object(
128+
self, name: str, path: str, schema: Dict[str, Any], root: Optional[AllTypes] = None
129+
) -> Object:
128130
_, is_nullable = self.__is_field_nullable(schema)
129131
model = Object.from_dict(
130132
{
@@ -136,21 +138,25 @@ def __parse_object(self, name: str, path: str, schema: Dict[str, Any]) -> Object
136138
**schema,
137139
}
138140
)
139-
self.root = model if not self.root else self.root
141+
root = model if root is None else root
140142
props = []
141143
for _name, definition in schema.get("properties", {}).items():
142-
props.append(self.__parse_definition(_name, path=f"{path}/{_name}", schema=definition))
144+
props.append(
145+
self.__parse_definition(_name, path=f"{path}/{_name}", schema=definition, root=root)
146+
)
143147
model.properties = props
144148
pattern_props = []
145149
for _name, definition in schema.get("patternProperties", {}).items():
146150
pattern_props.append(
147-
self.__parse_definition(_name, path=f"{path}/{_name}", schema=definition)
151+
self.__parse_definition(_name, path=f"{path}/{_name}", schema=definition, root=root)
148152
)
149153
model.patternProperties = pattern_props
150154

151155
return model
152156

153-
def __parse_array(self, name: str, path: str, schema: Dict[str, Any]) -> Array:
157+
def __parse_array(
158+
self, name: str, path: str, schema: Dict[str, Any], root: Optional[AllTypes] = None
159+
) -> Array:
154160
_, is_nullable = self.__is_field_nullable(schema)
155161
arr = Array.from_dict(
156162
{
@@ -162,11 +168,13 @@ def __parse_array(self, name: str, path: str, schema: Dict[str, Any]) -> Array:
162168
**schema,
163169
}
164170
)
165-
self.root = arr if not self.root else self.root
166-
arr.items = self.__parse_definition(name, f"{path}/items", schema["items"])
171+
root = arr if root is None else root
172+
arr.items = self.__parse_definition(name, f"{path}/items", schema["items"], root=root)
167173
return arr
168174

169-
def __parse_tuple(self, name: str, path: str, schema: Dict[str, Any]) -> JSFTuple:
175+
def __parse_tuple(
176+
self, name: str, path: str, schema: Dict[str, Any], root: Optional[AllTypes] = None
177+
) -> JSFTuple:
170178
_, is_nullable = self.__is_field_nullable(schema)
171179
arr = JSFTuple.from_dict(
172180
{
@@ -178,10 +186,12 @@ def __parse_tuple(self, name: str, path: str, schema: Dict[str, Any]) -> JSFTupl
178186
**schema,
179187
}
180188
)
181-
self.root = arr if not self.root else self.root
189+
root = arr if root is None else root
182190
arr.items = []
183191
for i, item in enumerate(schema["items"]):
184-
arr.items.append(self.__parse_definition(name, path=f"{path}/{name}[{i}]", schema=item))
192+
arr.items.append(
193+
self.__parse_definition(name, path=f"{path}/{name}[{i}]", schema=item, root=root)
194+
)
185195
return arr
186196

187197
def __is_field_nullable(self, schema: Dict[str, Any]) -> Tuple[str, bool]:
@@ -196,46 +206,55 @@ def __is_field_nullable(self, schema: Dict[str, Any]) -> Tuple[str, bool]:
196206
return random.choice(item_type_deep_copy), False
197207
return item_type, False
198208

199-
def __parse_anyOf(self, name: str, path: str, schema: Dict[str, Any]) -> AnyOf:
209+
def __parse_anyOf(
210+
self, name: str, path: str, schema: Dict[str, Any], root: Optional[AllTypes] = None
211+
) -> AnyOf:
200212
model = AnyOf(name=name, path=path, max_recursive_depth=self.max_recursive_depth, **schema)
201-
self.root = model if not self.root else self.root
213+
root = model if root is None else root
202214
schemas = []
203215
for d in schema["anyOf"]:
204-
schemas.append(self.__parse_definition(name, path, d))
216+
schemas.append(self.__parse_definition(name, path, d, root=root))
205217
model.schemas = schemas
206218
return model
207219

208-
def __parse_allOf(self, name: str, path: str, schema: Dict[str, Any]) -> AllOf:
220+
def __parse_allOf(
221+
self, name: str, path: str, schema: Dict[str, Any], root: Optional[AllTypes] = None
222+
) -> AllOf:
209223
combined_schema = dict(ChainMap(*schema["allOf"]))
210224
model = AllOf(name=name, path=path, max_recursive_depth=self.max_recursive_depth, **schema)
211-
self.root = model if not self.root else self.root
212-
model.combined_schema = self.__parse_definition(name, path, combined_schema)
225+
root = model if root is None else root
226+
model.combined_schema = self.__parse_definition(name, path, combined_schema, root=root)
213227
return model
214228

215-
def __parse_oneOf(self, name: str, path: str, schema: Dict[str, Any]) -> OneOf:
229+
def __parse_oneOf(
230+
self, name: str, path: str, schema: Dict[str, Any], root: Optional[AllTypes] = None
231+
) -> OneOf:
216232
model = OneOf(name=name, path=path, max_recursive_depth=self.max_recursive_depth, **schema)
217-
self.root = model if not self.root else self.root
233+
root = model if root is None else root
218234
schemas = []
219235
for d in schema["oneOf"]:
220-
schemas.append(self.__parse_definition(name, path, d))
236+
schemas.append(self.__parse_definition(name, path, d, root=root))
221237
model.schemas = schemas
222238
return model
223239

224-
def __parse_named_definition(self, path: str, def_name: str) -> AllTypes:
240+
def __parse_named_definition(self, path: str, def_name: str, root) -> AllTypes:
225241
schema = self.root_schema
226242
parsed_definition = None
227243
for def_tag in ("definitions", "$defs"):
228244
if path.startswith(f"#/{def_tag}/{def_name}"):
229-
self.root.is_recursive = True
230-
return self.root
231-
elif definition := schema.get(def_tag, {}).get(def_name):
245+
root.is_recursive = True
246+
return root
247+
definition = schema.get(def_tag, {}).get(def_name)
248+
if definition is not None:
232249
parsed_definition = self.__parse_definition(
233-
def_name, path=f"{path}/#/{def_tag}/{def_name}", schema=definition
250+
def_name, path=f"{path}/#/{def_tag}/{def_name}", schema=definition, root=root
234251
)
235252
self.definitions[f"#/{def_tag}/{def_name}"] = parsed_definition
236253
return parsed_definition
237254

238-
def __parse_definition(self, name: str, path: str, schema: Dict[str, Any]) -> AllTypes:
255+
def __parse_definition(
256+
self, name: str, path: str, schema: Dict[str, Any], root: Optional[AllTypes] = None
257+
) -> AllTypes:
239258
self.base_state["__all_json_paths__"].append(path)
240259
item_type, is_nullable = self.__is_field_nullable(schema)
241260
if "const" in schema:
@@ -259,20 +278,20 @@ def __parse_definition(self, name: str, path: str, schema: Dict[str, Any]) -> Al
259278
)
260279
elif "type" in schema:
261280
if item_type == "object" and "properties" in schema:
262-
return self.__parse_object(name, path, schema)
281+
return self.__parse_object(name, path, schema, root)
263282
elif item_type == "object" and "anyOf" in schema:
264-
return self.__parse_anyOf(name, path, schema)
283+
return self.__parse_anyOf(name, path, schema, root)
265284
elif item_type == "object" and "allOf" in schema:
266-
return self.__parse_allOf(name, path, schema)
285+
return self.__parse_allOf(name, path, schema, root)
267286
elif item_type == "object" and "oneOf" in schema:
268-
return self.__parse_oneOf(name, path, schema)
287+
return self.__parse_oneOf(name, path, schema, root)
269288
elif item_type == "array":
270289
if (schema.get("contains") is not None) or isinstance(schema.get("items"), dict):
271-
return self.__parse_array(name, path, schema)
290+
return self.__parse_array(name, path, schema, root)
272291
if isinstance(schema.get("items"), list) and all(
273292
isinstance(x, dict) for x in schema.get("items", [])
274293
):
275-
return self.__parse_tuple(name, path, schema)
294+
return self.__parse_tuple(name, path, schema, root)
276295
else:
277296
return self.__parse_primitive(name, path, schema)
278297
elif "$ref" in schema:
@@ -283,20 +302,23 @@ def __parse_definition(self, name: str, path: str, schema: Dict[str, Any]) -> Al
283302
else:
284303
# parse referenced definition
285304
ref_name = frag.split("/")[-1]
286-
cls = self.__parse_named_definition(path, ref_name)
305+
cls = self.__parse_named_definition(path, ref_name, root)
287306
else:
288307
with s_open(ext, "r") as f:
289308
external_jsf = JSF(json.load(f))
290309
cls = deepcopy(external_jsf.definitions.get(f"#{frag}"))
291-
cls.name = name
292-
cls.path = path
310+
if path != "#" and cls == root:
311+
cls.name = name
312+
elif path != "#":
313+
cls.name = name
314+
cls.path = path
293315
return cls
294316
elif "anyOf" in schema:
295-
return self.__parse_anyOf(name, path, schema)
317+
return self.__parse_anyOf(name, path, schema, root)
296318
elif "allOf" in schema:
297-
return self.__parse_allOf(name, path, schema)
319+
return self.__parse_allOf(name, path, schema, root)
298320
elif "oneOf" in schema:
299-
return self.__parse_oneOf(name, path, schema)
321+
return self.__parse_oneOf(name, path, schema, root)
300322
else:
301323
raise ValueError(f"Cannot parse schema {repr(schema)}") # pragma: no cover
302324

jsf/tests/data/anyof_recursive.json

-24
This file was deleted.

jsf/tests/data/complex_recursive.json

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"$ref": "#/definitions/tree",
3+
"definitions": {
4+
"tree": {
5+
"anyOf": [
6+
{
7+
"$ref": "#/definitions/node"
8+
},
9+
{
10+
"type": "string"
11+
}
12+
]
13+
},
14+
"node": {
15+
"type": "object",
16+
"allOf": [
17+
{
18+
"type": "object",
19+
"oneOf": [
20+
{
21+
"type": "object",
22+
"properties": {
23+
"value": {
24+
"$ref": "#/definitions/tree"
25+
}
26+
},
27+
"required": [
28+
"value"
29+
]
30+
},
31+
{
32+
"type": "object",
33+
"properties": {
34+
"value": {
35+
"type": "string"
36+
}
37+
},
38+
"required": ["value"]
39+
}
40+
]
41+
}
42+
]
43+
}
44+
}
45+
}

jsf/tests/test_default_fake.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -488,14 +488,13 @@ def test_fake_oneof_recursive(TestData):
488488
assert isinstance(item, int) or isinstance(item, list)
489489

490490

491-
def test_fake_anyof_recursive(TestData):
492-
with open(TestData / "anyof_recursive.json") as file:
491+
def test_fake_complex_recursive(TestData):
492+
with open(TestData / "complex_recursive.json") as file:
493493
schema = json.load(file)
494494
p = JSF(schema, max_recursive_depth=2)
495495

496496
fake_data = [p.generate() for _ in range(10)]
497497
for d in fake_data:
498-
for item in d:
499-
assert isinstance(item, str) or isinstance(item, dict)
500-
if isinstance(item, dict):
501-
assert "value" in item
498+
assert isinstance(d, str) or isinstance(d, dict)
499+
if isinstance(d, dict):
500+
assert "value" in d

0 commit comments

Comments
 (0)