Skip to content

Commit

Permalink
Merge pull request #266 from rmspeers/x-anyOf
Browse files Browse the repository at this point in the history
Setup for handling anyOf simplifying to oneOf on import.
  • Loading branch information
cwacek authored Dec 6, 2023
2 parents 141dc1a + a201148 commit 72e2ebf
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 6 deletions.
13 changes: 11 additions & 2 deletions python_jsonschema_objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,13 @@ def validate(self, obj):
except jsonschema.ValidationError as e:
raise ValidationError(e)

def build_classes(self, strict=False, named_only=False, standardize_names=True):
def build_classes(
self,
strict=False,
named_only=False,
standardize_names=True,
any_of: typing.Optional[typing.Literal["use-first"]] = None,
):
"""
Build all of the classes named in the JSONSchema.
Expand All @@ -201,12 +207,15 @@ def build_classes(self, strict=False, named_only=False, standardize_names=True):
generated).
standardize_names: (bool) If true (the default), class names will be
transformed by camel casing
any_of: (literal) If not set to None, defines the way anyOf clauses are resolved:
- 'use-first': Generate to the first matching schema in the list under the anyOf
- None: default behavior, anyOf is not supported in the schema
Returns:
A namespace containing all the generated classes
"""
kw = {"strict": strict}
kw = {"strict": strict, "any_of": any_of}
builder = classbuilder.ClassBuilder(self.resolver)
for nm, defn in six.iteritems(self.schema.get("definitions", {})):
resolved = self.resolver.lookup("#/definitions/" + nm)
Expand Down
21 changes: 17 additions & 4 deletions python_jsonschema_objects/classbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,6 @@ def validate(self):
for prop, val in six.iteritems(self._properties):
if val is None:
continue

if isinstance(val, ProtocolBase):
val.validate()
elif getattr(val, "isLiteralClass", None) is True:
Expand Down Expand Up @@ -502,9 +501,22 @@ def construct(self, uri, *args, **kw):

def _construct(self, uri, clsdata, parent=(ProtocolBase,), **kw):
if "anyOf" in clsdata:
raise NotImplementedError("anyOf is not supported as bare property")
if kw.get("any_of", None) is None:
raise NotImplementedError(
"anyOf is not supported as bare property (workarounds available by setting any_of flag)"
)
if kw["any_of"] == "use-first":
# Patch so the first anyOf becomes a single oneOf
clsdata["oneOf"] = [
clsdata["anyOf"].pop(0),
]
del clsdata["anyOf"]
else:
raise NotImplementedError(
f"anyOf workaround is not a recognized type (any_of = {kw['any_of']})"
)

elif "oneOf" in clsdata:
if "oneOf" in clsdata:
"""If this object itself has a 'oneOf' designation,
then construct a TypeProxy.
"""
Expand Down Expand Up @@ -561,7 +573,7 @@ def _construct(self, uri, clsdata, parent=(ProtocolBase,), **kw):
uri,
item_constraint=clsdata_copy.pop("items"),
classbuilder=self,
**clsdata_copy
**clsdata_copy,
)
return self.resolved[uri]

Expand Down Expand Up @@ -699,6 +711,7 @@ def _build_object(self, nm, clsdata, parents, **kw):
else:
uri = "{0}/{1}_{2}".format(nm, prop, "<anonymous_field>")
try:
# NOTE: Currently anyOf workaround is applied on import, not here for serialization
if "oneOf" in detail["items"]:
typ = TypeProxy(
self.construct_objects(
Expand Down
70 changes: 70 additions & 0 deletions test/test_feature_51.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import pytest

import python_jsonschema_objects as pjo


def test_simple_array_anyOf():
basicSchemaDefn = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Test",
"properties": {"ExampleAnyOf": {"$ref": "#/definitions/exampleAnyOf"}},
"required": ["ExampleAnyOf"],
"type": "object",
"definitions": {
"exampleAnyOf": {
# "type": "string", "format": "email"
"anyOf": [
{"type": "string", "format": "email"},
{"type": "string", "maxlength": 0},
]
}
},
}

builder = pjo.ObjectBuilder(basicSchemaDefn)

ns = builder.build_classes(any_of="use-first")
ns.Test().from_json('{"ExampleAnyOf" : "[email protected]"}')

with pytest.raises(pjo.ValidationError):
# Because string maxlength 0 is not selected, as we are using the first validation in anyOf:
ns.Test().from_json('{"ExampleAnyOf" : ""}')
# Because this does not match the email format:
ns.Test().from_json('{"ExampleAnyOf" : "not-an-email"}')

# Does it also work when not deserializing?
x = ns.Test()
with pytest.raises(pjo.ValidationError):
x.ExampleAnyOf = ""

with pytest.raises(pjo.ValidationError):
x.ExampleAnyOf = "not-an-email"

x.ExampleAnyOf = "[email protected]"
out = x.serialize()
y = ns.Test.from_json(out)
assert y.ExampleAnyOf == "[email protected]"


def test_simple_array_anyOf_withoutConfig():
basicSchemaDefn = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Test",
"properties": {"ExampleAnyOf": {"$ref": "#/definitions/exampleAnyOf"}},
"required": ["ExampleAnyOf"],
"type": "object",
"definitions": {
"exampleAnyOf": {
# "type": "string", "format": "email"
"anyOf": [
{"type": "string", "format": "email"},
{"type": "string", "maxlength": 0},
]
}
},
}

builder = pjo.ObjectBuilder(basicSchemaDefn)

with pytest.raises(NotImplementedError):
builder.build_classes()

0 comments on commit 72e2ebf

Please sign in to comment.