(feature): Fix OneOf behavior on invalid discriminator
According to the spec, propertyName is required when using a discriminator. If it is missing, the schema is invalid and should throw.
This commit is contained in:
@@ -354,131 +354,86 @@ class TestOneOfTypeParser(TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
Model = SchemaConverter.build(schema)
|
||||
# Should throw because the spec determines propertyName is required for discriminator
|
||||
with self.assertRaises(ValueError):
|
||||
SchemaConverter.build(schema)
|
||||
|
||||
# Should succeed because input matches exactly one schema (the first one)
|
||||
# The first schema matches: type="a" matches const("a"), value="test" is a string
|
||||
# The second schema doesn't match: type="a" does not match const("b")
|
||||
obj = Model(value={"type": "a", "value": "test", "extra": "invalid"})
|
||||
self.assertEqual(obj.value.type, "a")
|
||||
self.assertEqual(obj.value.value, "test")
|
||||
|
||||
# Test with input that matches the second schema
|
||||
obj2 = Model(value={"type": "b", "value": 42})
|
||||
self.assertEqual(obj2.value.type, "b")
|
||||
self.assertEqual(obj2.value.value, 42)
|
||||
|
||||
# Test with input that matches neither schema (should fail)
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
Model(value={"type": "c", "value": "test"})
|
||||
self.assertIn("does not match any of the oneOf schemas", str(cm.exception))
|
||||
|
||||
def test_oneof_multiple_matches_without_discriminator(self):
|
||||
"""Test case where input genuinely matches multiple oneOf schemas"""
|
||||
def test_oneof_overlapping_strings_from_docs(self):
|
||||
"""Test the overlapping strings example from documentation"""
|
||||
schema = {
|
||||
"title": "Test",
|
||||
"title": "SimpleExample",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{"type": "object", "properties": {"data": {"type": "string"}}},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {"type": "string"},
|
||||
"optional": {"type": "string"},
|
||||
},
|
||||
},
|
||||
],
|
||||
"discriminator": {}, # discriminator without propertyName
|
||||
{"type": "string", "maxLength": 6},
|
||||
{"type": "string", "minLength": 4},
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["value"],
|
||||
}
|
||||
|
||||
Model = SchemaConverter.build(schema)
|
||||
|
||||
# This input matches both schemas since both accept data as string
|
||||
# and neither requires specific additional properties
|
||||
# Valid: Short string (matches first schema only)
|
||||
obj1 = Model(value="hi")
|
||||
self.assertEqual(obj1.value, "hi")
|
||||
|
||||
# Valid: Long string (matches second schema only)
|
||||
obj2 = Model(value="very long string")
|
||||
self.assertEqual(obj2.value, "very long string")
|
||||
|
||||
# Invalid: Medium string (matches BOTH schemas - violates oneOf)
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
Model(value={"data": "test"})
|
||||
Model(value="hello") # 5 chars: matches maxLength=6 AND minLength=4
|
||||
self.assertIn("matches multiple oneOf schemas", str(cm.exception))
|
||||
|
||||
def test_oneof_overlapping_strings_from_docs(self):
|
||||
"""Test the overlapping strings example from documentation"""
|
||||
schema = {
|
||||
"title": "SimpleExample",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{"type": "string", "maxLength": 6},
|
||||
{"type": "string", "minLength": 4},
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["value"],
|
||||
}
|
||||
|
||||
Model = SchemaConverter.build(schema)
|
||||
|
||||
# Valid: Short string (matches first schema only)
|
||||
obj1 = Model(value="hi")
|
||||
self.assertEqual(obj1.value, "hi")
|
||||
|
||||
# Valid: Long string (matches second schema only)
|
||||
obj2 = Model(value="very long string")
|
||||
self.assertEqual(obj2.value, "very long string")
|
||||
|
||||
# Invalid: Medium string (matches BOTH schemas - violates oneOf)
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
Model(value="hello") # 5 chars: matches maxLength=6 AND minLength=4
|
||||
self.assertIn("matches multiple oneOf schemas", str(cm.exception))
|
||||
|
||||
def test_oneof_shapes_discriminator_from_docs(self):
|
||||
"""Test the shapes discriminator example from documentation"""
|
||||
schema = {
|
||||
"title": "Shape",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"shape": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"const": "circle"},
|
||||
"radius": {"type": "number", "minimum": 0},
|
||||
},
|
||||
"required": ["type", "radius"],
|
||||
def test_oneof_shapes_discriminator_from_docs(self):
|
||||
"""Test the shapes discriminator example from documentation"""
|
||||
schema = {
|
||||
"title": "Shape",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"shape": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"const": "circle"},
|
||||
"radius": {"type": "number", "minimum": 0},
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"const": "rectangle"},
|
||||
"width": {"type": "number", "minimum": 0},
|
||||
"height": {"type": "number", "minimum": 0},
|
||||
},
|
||||
"required": ["type", "width", "height"],
|
||||
"required": ["type", "radius"],
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"const": "rectangle"},
|
||||
"width": {"type": "number", "minimum": 0},
|
||||
"height": {"type": "number", "minimum": 0},
|
||||
},
|
||||
],
|
||||
"discriminator": {"propertyName": "type"},
|
||||
}
|
||||
},
|
||||
"required": ["shape"],
|
||||
}
|
||||
"required": ["type", "width", "height"],
|
||||
},
|
||||
],
|
||||
"discriminator": {"propertyName": "type"},
|
||||
}
|
||||
},
|
||||
"required": ["shape"],
|
||||
}
|
||||
|
||||
Model = SchemaConverter.build(schema)
|
||||
Model = SchemaConverter.build(schema)
|
||||
|
||||
# Valid: Circle
|
||||
circle = Model(shape={"type": "circle", "radius": 5.0})
|
||||
self.assertEqual(circle.shape.type, "circle")
|
||||
self.assertEqual(circle.shape.radius, 5.0)
|
||||
# Valid: Circle
|
||||
circle = Model(shape={"type": "circle", "radius": 5.0})
|
||||
self.assertEqual(circle.shape.type, "circle")
|
||||
self.assertEqual(circle.shape.radius, 5.0)
|
||||
|
||||
# Valid: Rectangle
|
||||
rectangle = Model(shape={"type": "rectangle", "width": 10, "height": 20})
|
||||
self.assertEqual(rectangle.shape.type, "rectangle")
|
||||
self.assertEqual(rectangle.shape.width, 10)
|
||||
self.assertEqual(rectangle.shape.height, 20)
|
||||
# Valid: Rectangle
|
||||
rectangle = Model(shape={"type": "rectangle", "width": 10, "height": 20})
|
||||
self.assertEqual(rectangle.shape.type, "rectangle")
|
||||
self.assertEqual(rectangle.shape.width, 10)
|
||||
self.assertEqual(rectangle.shape.height, 20)
|
||||
|
||||
# Invalid: Wrong properties for the type
|
||||
with self.assertRaises(ValueError):
|
||||
Model(shape={"type": "circle", "width": 10})
|
||||
# Invalid: Wrong properties for the type
|
||||
with self.assertRaises(ValueError):
|
||||
Model(shape={"type": "circle", "width": 10})
|
||||
|
||||
Reference in New Issue
Block a user