Skip to content

Commit 5849f2e

Browse files
authored
[yaml] : fix validate compatible method (#37588)
* first draft of changes * additionalProperties * fix weak schema logic * correct some logic
1 parent 8da478b commit 5849f2e

2 files changed

Lines changed: 144 additions & 10 deletions

File tree

sdks/python/apache_beam/yaml/json_utils.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def maybe_nullable(beam_type, nullable):
6363

6464
json_type = json_schema.get('type', None)
6565
if json_type != 'object':
66-
raise ValueError('Expected object type, got {json_type}.')
66+
raise ValueError(f'Expected object type, got {json_type}.')
6767
if 'properties' not in json_schema:
6868
# Technically this is a valid (vacuous) schema, but as it's not generally
6969
# meaningful, throw an informative error instead.
@@ -314,24 +314,34 @@ def _validate_compatible(weak_schema, strong_schema):
314314
return
315315
if weak_schema['type'] != strong_schema['type']:
316316
raise ValueError(
317-
'Incompatible types: %r vs %r' %
318-
(weak_schema['type'] != strong_schema['type']))
317+
f"Incompatible types: {weak_schema['type']} vs {strong_schema['type']}")
319318
if weak_schema['type'] == 'array':
320319
_validate_compatible(weak_schema['items'], strong_schema['items'])
321-
elif weak_schema == 'object':
320+
elif weak_schema['type'] == 'object':
321+
# If the weak schema allows for arbitrary keys (is a map),
322+
# the strong schema must also allow for arbitrary keys.
323+
if weak_schema.get('additionalProperties'):
324+
if not strong_schema.get('additionalProperties', True):
325+
raise ValueError('Incompatible types: map vs object')
326+
_validate_compatible(
327+
weak_schema['additionalProperties'],
328+
strong_schema['additionalProperties'])
322329
for required in strong_schema.get('required', []):
323330
if required not in weak_schema['properties']:
324-
raise ValueError('Missing or unkown property %r' % required)
325-
for name, spec in weak_schema.get('properties', {}):
331+
raise ValueError(f"Missing or unknown property '{required}'")
332+
for name, spec in weak_schema.get('properties', {}).items():
333+
326334
if name in strong_schema['properties']:
327335
try:
328336
_validate_compatible(spec, strong_schema['properties'][name])
329337
except Exception as exn:
330-
raise ValueError('Incompatible schema for %r' % name) from exn
331-
elif not strong_schema.get('additionalProperties'):
338+
raise ValueError(f"Incompatible schema for '{name}'") from exn
339+
elif not strong_schema.get('additionalProperties', True):
340+
# The property is not explicitly in the strong schema, and the strong
341+
# schema does not allow for extra properties.
332342
raise ValueError(
333-
'Prohibited property: {property}; '
334-
'perhaps additionalProperties: False is missing?')
343+
f"Prohibited property: '{name}'; "
344+
"perhaps additionalProperties: False is missing?")
335345

336346

337347
def row_validator(beam_schema: schema_pb2.Schema,

sdks/python/apache_beam/yaml/json_utils_test.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,130 @@ def test_json_to_row_with_missing_required_field(self):
152152
with self.assertRaises(KeyError):
153153
converter(json_data)
154154

155+
def test_validate_compatible(self):
156+
from apache_beam.yaml.json_utils import _validate_compatible
157+
158+
# Compatible cases
159+
_validate_compatible({'type': 'string'}, {'type': 'string'})
160+
_validate_compatible(
161+
{
162+
'type': 'object', 'properties': {
163+
'f': {
164+
'type': 'string'
165+
}
166+
}
167+
}, {
168+
'type': 'object', 'properties': {
169+
'f': {
170+
'type': 'string'
171+
}
172+
}
173+
})
174+
175+
# Incompatible types
176+
with self.assertRaisesRegex(ValueError, 'Incompatible types'):
177+
_validate_compatible({'type': 'string'}, {'type': 'integer'})
178+
179+
# Missing property
180+
with self.assertRaisesRegex(ValueError, 'Missing or unknown property'):
181+
_validate_compatible({
182+
'type': 'object', 'properties': {}
183+
},
184+
{
185+
'type': 'object',
186+
'properties': {
187+
'f': {
188+
'type': 'string'
189+
}
190+
},
191+
'required': ['f']
192+
})
193+
194+
# Incompatible property type
195+
with self.assertRaisesRegex(ValueError, 'Incompatible schema for \'f\''):
196+
_validate_compatible(
197+
{
198+
'type': 'object', 'properties': {
199+
'f': {
200+
'type': 'integer'
201+
}
202+
}
203+
}, {
204+
'type': 'object', 'properties': {
205+
'f': {
206+
'type': 'string'
207+
}
208+
}
209+
})
210+
211+
def test_validate_compatible_map(self):
212+
from apache_beam.yaml.json_utils import _validate_compatible
213+
214+
# Compatible maps
215+
_validate_compatible(
216+
{
217+
'type': 'object', 'additionalProperties': {
218+
'type': 'string'
219+
}
220+
}, {
221+
'type': 'object', 'additionalProperties': {
222+
'type': 'string'
223+
}
224+
})
225+
226+
# Incompatible map values
227+
with self.assertRaisesRegex(ValueError, 'Incompatible types'):
228+
_validate_compatible(
229+
{
230+
'type': 'object', 'additionalProperties': {
231+
'type': 'string'
232+
}
233+
}, {
234+
'type': 'object', 'additionalProperties': {
235+
'type': 'integer'
236+
}
237+
})
238+
239+
# Map vs Object
240+
with self.assertRaisesRegex(ValueError,
241+
'Incompatible types: map vs object'):
242+
_validate_compatible(
243+
{
244+
'type': 'object', 'additionalProperties': {
245+
'type': 'string'
246+
}
247+
}, {
248+
'type': 'object', 'properties': {}, 'additionalProperties': False
249+
})
250+
251+
def test_validate_compatible_extra_properties(self):
252+
from apache_beam.yaml.json_utils import _validate_compatible
253+
254+
# Extra properties in weak_schema should be allowed if strong_schema
255+
# doesn't explicitly forbid them (default additionalProperties=True).
256+
_validate_compatible({
257+
'type': 'object', 'properties': {
258+
'extra': {
259+
'type': 'string'
260+
}
261+
}
262+
}, {
263+
'type': 'object', 'properties': {}
264+
})
265+
266+
# But if strong_schema says additionalProperties: False, it should raise.
267+
with self.assertRaisesRegex(ValueError, 'Prohibited property'):
268+
_validate_compatible(
269+
{
270+
'type': 'object', 'properties': {
271+
'extra': {
272+
'type': 'string'
273+
}
274+
}
275+
}, {
276+
'type': 'object', 'properties': {}, 'additionalProperties': False
277+
})
278+
155279

156280
if __name__ == '__main__':
157281
unittest.main()

0 commit comments

Comments
 (0)