Skip to content

Commit 1089706

Browse files
test: Explicitly test flattening of combined (oneOf, anyOf, allOf) schemas (#3413)
## Summary by Sourcery Tests: - Add a regression test ensuring flatten_schema correctly flattens schemas using oneOf, anyOf, and allOf combinations. --------- Signed-off-by: Edgar Ramírez Mondragón <[email protected]>
1 parent ce5e41d commit 1089706

File tree

2 files changed

+110
-12
lines changed

2 files changed

+110
-12
lines changed

singer_sdk/helpers/_flattening.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
DEFAULT_FLATTENING_SEPARATOR = "__"
1818
DEFAULT_MAX_KEY_LENGTH = 255
1919

20+
_T = t.TypeVar("_T")
21+
22+
23+
def _first(iterable: t.Iterable[_T]) -> _T | None:
24+
return next(iter(iterable), None)
25+
2026

2127
class PluginFlatteningConfig(t.TypedDict):
2228
"""Plugin flattening configuration."""
@@ -380,18 +386,22 @@ def _flatten_schema( # noqa: C901, PLR0912
380386
items.append((new_key, {"type": types}))
381387
else:
382388
items.append((new_key, field_schema))
383-
# TODO: Figure out what this really does, try breaking it.
384-
# If it's not needed, remove it.
385-
elif len(field_schema.values()) > 0:
386-
if next(iter(field_schema.values()))[0]["type"] == "string":
387-
next(iter(field_schema.values()))[0]["type"] = ["null", "string"]
388-
items.append((new_key, next(iter(field_schema.values()))[0]))
389-
elif next(iter(field_schema.values()))[0]["type"] == "array":
390-
next(iter(field_schema.values()))[0]["type"] = ["null", "array"]
391-
items.append((new_key, next(iter(field_schema.values()))[0]))
392-
elif next(iter(field_schema.values()))[0]["type"] == "object":
393-
next(iter(field_schema.values()))[0]["type"] = ["null", "object"]
394-
items.append((new_key, next(iter(field_schema.values()))[0]))
389+
# Handle oneOf, anyOf, etc.
390+
elif (
391+
(composite := _first(field_schema.values()))
392+
and isinstance(composite, list)
393+
and len(composite) > 0
394+
and (first_element := _first(composite))
395+
):
396+
if first_element["type"] == "string":
397+
first_element["type"] = ["null", "string"]
398+
items.append((new_key, first_element))
399+
elif first_element["type"] == "array":
400+
first_element["type"] = ["null", "array"]
401+
items.append((new_key, first_element))
402+
elif first_element["type"] == "object":
403+
first_element["type"] = ["null", "object"]
404+
items.append((new_key, first_element))
395405
else:
396406
# Handle typeless properties (e.g., "PropertyName": {})
397407
# Treat them as string type to allow JSON serialization

tests/core/test_flattening.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,94 @@ def test_flatten_record_with_typeless_property_values():
200200
assert flattened_record["changes__NewValue"] == "simple string"
201201

202202

203+
def test_flatten_combined_schemas():
204+
"""Test that combined schemas are flattened correctly.
205+
206+
Examples of combined schemas:
207+
- oneOf
208+
- anyOf
209+
- allOf
210+
211+
https://json-schema.org/understanding-json-schema/reference/combining
212+
"""
213+
schema = {
214+
"type": "object",
215+
"properties": {
216+
"name": {
217+
"oneOf": [
218+
{"type": "string"},
219+
{
220+
"type": "object",
221+
"properties": {
222+
"first_name": {"type": "string"},
223+
"last_name": {"type": "string"},
224+
},
225+
},
226+
],
227+
},
228+
"address": {
229+
"anyOf": [
230+
{
231+
"type": "object",
232+
"properties": {
233+
"street": {"type": "string"},
234+
"city": {"type": "string"},
235+
"state": {"type": "string"},
236+
"zip": {"type": "string"},
237+
},
238+
},
239+
],
240+
},
241+
"phones": {
242+
"allOf": [
243+
{
244+
"type": "array",
245+
"items": {
246+
"type": "object",
247+
"properties": {
248+
"type": {"type": "string"},
249+
"number": {"type": "string"},
250+
},
251+
},
252+
},
253+
],
254+
},
255+
"id": {
256+
"oneOf": [
257+
{"type": "integer"},
258+
{"type": "string", "format": "uuid"},
259+
],
260+
},
261+
},
262+
}
263+
flattened = flatten_schema(schema, max_level=1)
264+
assert flattened == {
265+
"type": "object",
266+
"properties": {
267+
"name": {"type": ["null", "string"]},
268+
"address": {
269+
"type": ["null", "object"],
270+
"properties": {
271+
"street": {"type": "string"},
272+
"city": {"type": "string"},
273+
"state": {"type": "string"},
274+
"zip": {"type": "string"},
275+
},
276+
},
277+
"phones": {
278+
"type": ["null", "array"],
279+
"items": {
280+
"type": "object",
281+
"properties": {
282+
"type": {"type": "string"},
283+
"number": {"type": "string"},
284+
},
285+
},
286+
},
287+
},
288+
}
289+
290+
203291
def test_flatten_key_with_long_names(subtests: pytest.Subtests):
204292
"""Test that flatten_key abbreviates long key names to stay under 255 chars.
205293

0 commit comments

Comments
 (0)