Skip to content

Commit 72e2ebf

Browse files
authored
Merge pull request #266 from rmspeers/x-anyOf
Setup for handling anyOf simplifying to oneOf on import.
2 parents 141dc1a + a201148 commit 72e2ebf

File tree

3 files changed

+98
-6
lines changed

3 files changed

+98
-6
lines changed

python_jsonschema_objects/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,13 @@ def validate(self, obj):
183183
except jsonschema.ValidationError as e:
184184
raise ValidationError(e)
185185

186-
def build_classes(self, strict=False, named_only=False, standardize_names=True):
186+
def build_classes(
187+
self,
188+
strict=False,
189+
named_only=False,
190+
standardize_names=True,
191+
any_of: typing.Optional[typing.Literal["use-first"]] = None,
192+
):
187193
"""
188194
Build all of the classes named in the JSONSchema.
189195
@@ -201,12 +207,15 @@ def build_classes(self, strict=False, named_only=False, standardize_names=True):
201207
generated).
202208
standardize_names: (bool) If true (the default), class names will be
203209
transformed by camel casing
210+
any_of: (literal) If not set to None, defines the way anyOf clauses are resolved:
211+
- 'use-first': Generate to the first matching schema in the list under the anyOf
212+
- None: default behavior, anyOf is not supported in the schema
204213
205214
Returns:
206215
A namespace containing all the generated classes
207216
208217
"""
209-
kw = {"strict": strict}
218+
kw = {"strict": strict, "any_of": any_of}
210219
builder = classbuilder.ClassBuilder(self.resolver)
211220
for nm, defn in six.iteritems(self.schema.get("definitions", {})):
212221
resolved = self.resolver.lookup("#/definitions/" + nm)

python_jsonschema_objects/classbuilder.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,6 @@ def validate(self):
320320
for prop, val in six.iteritems(self._properties):
321321
if val is None:
322322
continue
323-
324323
if isinstance(val, ProtocolBase):
325324
val.validate()
326325
elif getattr(val, "isLiteralClass", None) is True:
@@ -502,9 +501,22 @@ def construct(self, uri, *args, **kw):
502501

503502
def _construct(self, uri, clsdata, parent=(ProtocolBase,), **kw):
504503
if "anyOf" in clsdata:
505-
raise NotImplementedError("anyOf is not supported as bare property")
504+
if kw.get("any_of", None) is None:
505+
raise NotImplementedError(
506+
"anyOf is not supported as bare property (workarounds available by setting any_of flag)"
507+
)
508+
if kw["any_of"] == "use-first":
509+
# Patch so the first anyOf becomes a single oneOf
510+
clsdata["oneOf"] = [
511+
clsdata["anyOf"].pop(0),
512+
]
513+
del clsdata["anyOf"]
514+
else:
515+
raise NotImplementedError(
516+
f"anyOf workaround is not a recognized type (any_of = {kw['any_of']})"
517+
)
506518

507-
elif "oneOf" in clsdata:
519+
if "oneOf" in clsdata:
508520
"""If this object itself has a 'oneOf' designation,
509521
then construct a TypeProxy.
510522
"""
@@ -561,7 +573,7 @@ def _construct(self, uri, clsdata, parent=(ProtocolBase,), **kw):
561573
uri,
562574
item_constraint=clsdata_copy.pop("items"),
563575
classbuilder=self,
564-
**clsdata_copy
576+
**clsdata_copy,
565577
)
566578
return self.resolved[uri]
567579

@@ -699,6 +711,7 @@ def _build_object(self, nm, clsdata, parents, **kw):
699711
else:
700712
uri = "{0}/{1}_{2}".format(nm, prop, "<anonymous_field>")
701713
try:
714+
# NOTE: Currently anyOf workaround is applied on import, not here for serialization
702715
if "oneOf" in detail["items"]:
703716
typ = TypeProxy(
704717
self.construct_objects(

test/test_feature_51.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import pytest
2+
3+
import python_jsonschema_objects as pjo
4+
5+
6+
def test_simple_array_anyOf():
7+
basicSchemaDefn = {
8+
"$schema": "http://json-schema.org/draft-04/schema#",
9+
"title": "Test",
10+
"properties": {"ExampleAnyOf": {"$ref": "#/definitions/exampleAnyOf"}},
11+
"required": ["ExampleAnyOf"],
12+
"type": "object",
13+
"definitions": {
14+
"exampleAnyOf": {
15+
# "type": "string", "format": "email"
16+
"anyOf": [
17+
{"type": "string", "format": "email"},
18+
{"type": "string", "maxlength": 0},
19+
]
20+
}
21+
},
22+
}
23+
24+
builder = pjo.ObjectBuilder(basicSchemaDefn)
25+
26+
ns = builder.build_classes(any_of="use-first")
27+
ns.Test().from_json('{"ExampleAnyOf" : "[email protected]"}')
28+
29+
with pytest.raises(pjo.ValidationError):
30+
# Because string maxlength 0 is not selected, as we are using the first validation in anyOf:
31+
ns.Test().from_json('{"ExampleAnyOf" : ""}')
32+
# Because this does not match the email format:
33+
ns.Test().from_json('{"ExampleAnyOf" : "not-an-email"}')
34+
35+
# Does it also work when not deserializing?
36+
x = ns.Test()
37+
with pytest.raises(pjo.ValidationError):
38+
x.ExampleAnyOf = ""
39+
40+
with pytest.raises(pjo.ValidationError):
41+
x.ExampleAnyOf = "not-an-email"
42+
43+
x.ExampleAnyOf = "[email protected]"
44+
out = x.serialize()
45+
y = ns.Test.from_json(out)
46+
assert y.ExampleAnyOf == "[email protected]"
47+
48+
49+
def test_simple_array_anyOf_withoutConfig():
50+
basicSchemaDefn = {
51+
"$schema": "http://json-schema.org/draft-04/schema#",
52+
"title": "Test",
53+
"properties": {"ExampleAnyOf": {"$ref": "#/definitions/exampleAnyOf"}},
54+
"required": ["ExampleAnyOf"],
55+
"type": "object",
56+
"definitions": {
57+
"exampleAnyOf": {
58+
# "type": "string", "format": "email"
59+
"anyOf": [
60+
{"type": "string", "format": "email"},
61+
{"type": "string", "maxlength": 0},
62+
]
63+
}
64+
},
65+
}
66+
67+
builder = pjo.ObjectBuilder(basicSchemaDefn)
68+
69+
with pytest.raises(NotImplementedError):
70+
builder.build_classes()

0 commit comments

Comments
 (0)