Skip to content
Open
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
9 changes: 9 additions & 0 deletions .changeset/ignore_empty_object_default_on_models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
default: patch
---

# Do not drop a schema over an empty object default

A property whose type is a model schema could not carry `default: {}`. The parser rejected it with `ModelProperty cannot have a default value`, or with `Value {} is not valid, only None is allowed` when the model sat in a union. The generator warned, dropped the enclosing schema and every endpoint that referenced it, and still exited 0.

An empty object carries no information for a model default, so it is now ignored and the property generates with no default. This is how an inline `default: {}` on a property and a bare `$ref` with a sibling `default` already behave. A non-empty default is still rejected.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from end_to_end_tests.functional_tests.helpers import (
with_generated_client_fixture,
with_generated_code_imports,
)


@with_generated_client_fixture(
"""
components:
schemas:
Holder:
type: object
properties:
extras:
anyOf:
- type: object
additionalProperties: true
- type: "null"
default: {}
"""
)
@with_generated_code_imports(".models.Holder", ".types.UNSET")
class TestEmptyObjectDefaultOnAUnionMember:
"""An empty object default on a union member leaves the enclosing schema in place. The property generates
with no default."""

def test_model_generates_without_the_default(self, Holder, UNSET):
assert Holder().extras is UNSET

def test_explicit_value_is_kept(self, Holder):
assert Holder.from_dict({"extras": {"a": 1}}).to_dict() == {"extras": {"a": 1}}


@with_generated_client_fixture(
"""
components:
schemas:
Free:
type: object
additionalProperties: true
Holder:
type: object
properties:
extras:
allOf:
- $ref: "#/components/schemas/Free"
default: {}
"""
)
@with_generated_code_imports(".models.Holder", ".types.UNSET")
class TestEmptyObjectDefaultOnAReferencedModel:
"""The same holds when the model carrying the default is referenced through an ``allOf``."""

def test_model_generates_without_the_default(self, Holder, UNSET):
assert Holder().extras is UNSET
5 changes: 4 additions & 1 deletion openapi_python_client/parser/properties/model_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,11 @@ def build(

@classmethod
def convert_value(cls, value: Any) -> Value | PropertyError | None:
if isinstance(value, dict) and not value:
# An empty object adds nothing to the default, and rejecting it drops the whole schema.
return None
if value is not None:
return PropertyError(detail="ModelProperty cannot have a default value") # pragma: no cover
return PropertyError(detail="ModelProperty cannot have a default value")
return None

def __attrs_post_init__(self) -> None:
Expand Down
9 changes: 9 additions & 0 deletions tests/test_parser/test_properties/test_model_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ def test_get_base_type_string(self, model_property_factory):
m = model_property_factory()
assert m.get_base_type_string() == PythonCode("MyClass")

def test_convert_value_passes_none_through(self, model_property_factory):
assert model_property_factory().convert_value(None) is None

def test_convert_value_ignores_an_empty_object_default(self, model_property_factory):
assert model_property_factory().convert_value({}) is None

def test_convert_value_rejects_a_non_empty_default(self, model_property_factory):
assert isinstance(model_property_factory().convert_value({"a": 1}), PropertyError)


class TestBuild:
@pytest.mark.parametrize(
Expand Down