Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions singer_sdk/helpers/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ def append_type(type_dict: dict, new_type: str) -> dict:
"""Return a combined type definition using the 'anyOf' JSON Schema construct."""
result = copy.deepcopy(type_dict)
if "anyOf" in result:
if isinstance(result["anyOf"], list) and new_type not in result["anyOf"]:
result["anyOf"].append(new_type)
elif new_type != result["anyOf"]:
result["anyOf"] = [result["anyOf"], new_type]
new_type_dict = {"type": new_type}
if isinstance(result["anyOf"], list):
if new_type_dict not in result["anyOf"]:
result["anyOf"].append(new_type_dict)
elif new_type_dict != result["anyOf"]:
result["anyOf"] = [result["anyOf"], new_type_dict]
return result

if "oneOf" in result:
Expand Down
78 changes: 76 additions & 2 deletions tests/core/test_jsonschema_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from singer_sdk.typing import (
DEFAULT_JSONSCHEMA_VALIDATOR,
AllOf,
AnyOf,
AnyType,
ArrayType,
BooleanType,
Expand All @@ -49,6 +50,7 @@
IRIType,
JSONPointerType,
ObjectType,
OneOf,
PropertiesList,
Property,
RegexType,
Expand Down Expand Up @@ -641,9 +643,10 @@ def test_property_creation(
) -> None:
property_dict = property_obj.to_dict()
assert property_dict == expected_jsonschema
property_name = next(iter(property_dict.keys()))
property_node = property_dict[property_name]
DEFAULT_JSONSCHEMA_VALIDATOR.check_schema(property_node)
for check_fn in TYPE_FN_CHECKS:
property_name = next(iter(property_dict.keys()))
property_node = property_dict[property_name]
if check_fn in type_fn_checks_true:
assert check_fn(property_node) is True, (
f"{check_fn.__name__} was not True for {property_dict!r}"
Expand All @@ -654,6 +657,77 @@ def test_property_creation(
)


@pytest.mark.parametrize(
"property_obj,expected_jsonschema",
[
pytest.param(
Property(
"my_prop",
AnyOf(StringType, ArrayType(StringType)),
required=True,
),
{
"my_prop": {
"anyOf": [
{"type": ["string"]},
{"type": "array", "items": {"type": ["string"]}},
],
},
},
id="anyof_required",
),
pytest.param(
Property(
"my_prop",
AnyOf(StringType, ArrayType(StringType)),
required=False,
),
{
"my_prop": {
"anyOf": [
{"type": ["string"]},
{"type": "array", "items": {"type": ["string"]}},
{"type": "null"},
],
},
},
id="anyof_optional",
),
pytest.param(
Property(
"my_prop",
OneOf(StringType, IntegerType),
required=False,
),
{
"my_prop": {
"oneOf": [
{"type": ["string"]},
{"type": ["integer"]},
{"type": "null"},
],
},
},
id="oneof_optional",
),
],
)
def test_optional_composite_type_produces_valid_schema(
property_obj: Property,
expected_jsonschema: dict,
) -> None:
"""Optional `AnyOf`/`OneOf` properties must stay valid JSON Schema.

Regression test: `append_type` used to splice the bare string "null" into
an existing `anyOf`/`oneOf` list instead of a `{"type": "null"}` subschema,
which fails JSON Schema meta-validation.
"""
property_dict = property_obj.to_dict()
assert property_dict == expected_jsonschema
property_name = next(iter(property_dict.keys()))
DEFAULT_JSONSCHEMA_VALIDATOR.check_schema(property_dict[property_name])


def test_wrapped_type_dict():
with pytest.raises(
ValueError,
Expand Down
2 changes: 1 addition & 1 deletion tests/core/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ def test_conform_primitives(value: t.Any, type_dict: dict, expected: t.Any):
pytest.param({"type": "array"}, {"type": ["array", "null"]}, id="array"),
pytest.param(
{"anyOf": [{"type": "integer"}, {"type": "number"}]},
{"anyOf": [{"type": "integer"}, {"type": "number"}, "null"]},
{"anyOf": [{"type": "integer"}, {"type": "number"}, {"type": "null"}]},
id="anyOf",
),
pytest.param(
Expand Down
Loading