Skip to content

fix: support default {} on freeform object schemas#1449

Open
alexander-wenzel-dev wants to merge 1 commit into
openapi-generators:mainfrom
alexander-wenzel-dev:fix/freeform-object-default
Open

fix: support default {} on freeform object schemas#1449
alexander-wenzel-dev wants to merge 1 commit into
openapi-generators:mainfrom
alexander-wenzel-dev:fix/freeform-object-default

Conversation

@alexander-wenzel-dev
Copy link
Copy Markdown

fix: support default {} on freeform object schemas

Summary

A freeform object schema (type: object, additionalProperties: true,
no declared properties) inside an anyOf could not carry a
default: {}. The parser rejected the default, which silently dropped
the enclosing schema and every endpoint that referenced it. This PR
accepts the empty-dict default and generates a working empty-container
initializer.

Problem

A schema declared as type: object with additionalProperties: true
and no declared properties (often appearing inside an anyOf with
null) could not carry a default: {}. ModelProperty.convert_value
rejected the default with ModelProperty cannot have a default value,
which propagated up as a warning and silently dropped the enclosing
schema and every endpoint that referenced it.

Minimal reproduction

components:
  schemas:
    ModelWithFreeformDefault:
      type: object
      properties:
        extras:
          anyOf:
            - type: object
              additionalProperties: true
            - type: "null"
          default: {}

Before this PR: ModelWithFreeformDefault is missing from the
generated client; the only signal is a warning on stderr.

Fix

Three coordinated changes in model_property.py and union.py:

  1. ModelProperty.convert_value accepts {} on a freeform model
    and emits a ClassName() constructor call. When
    required_properties has not yet been populated, the check falls
    back to the raw schema (data.properties, data.allOf,
    data.anyOf, data.oneOf) so a referenced schema still gets the
    correct decision before its properties have been processed.

  2. Import promotion in ModelProperty. A default value forces the
    generated client to import the inner model at runtime rather than
    only under TYPE_CHECKING, otherwise the default initializer would
    NameError at class-definition time. get_imports and
    get_lazy_imports are updated so inner-property imports are
    promoted when the property has a non-None default.

  3. Import promotion in UnionProperty. The same promotion applies
    to unions that carry a default — every inner property's lazy
    imports become runtime imports so the union's default expression
    resolves at class-definition time.

Tests

  • Functional test:
    end_to_end_tests/functional_tests/generated_code_execution/test_freeform_object_defaults.py
    — uses the inline spec above, asserts the model is instantiable
    and that the default extras is the empty container.
  • Unit tests in
    tests/test_parser/test_properties/test_model_property.py:
    • test_get_imports_with_default and test_get_lazy_imports_with_default
      cover the import-promotion branches.
    • test_convert_value and test_convert_value_unprocessed cover
      convert_value's new accept/reject decisions, including the
      pre-processing fallback path.

Local verification

pdm install
pdm run ruff format . --check
pdm run ruff check .
pdm mypy --show-error-codes
pdm test_with_coverage

All green on Python 3.14. 100% coverage on both changed files
(model_property.py, union.py) when measured across the full
upstream suite. Golden-record snapshot tests pass without
regeneration — the import-promotion paths are only reachable when a
model has a non-None default, which previously errored, so no
existing snapshot can depend on the old behaviour.

Risk

  • ModelProperty.convert_value changes from a @classmethod to an
    instance method.
    A grep across openapi_python_client/, tests/,
    and end_to_end_tests/ finds zero ModelProperty.convert_value(...)
    class-level call sites; every caller uses prop.convert_value(...)
    via the PropertyProtocol. The signature change is invisible to
    existing call sites.
  • Acceptance is conservative: only {} is accepted, and only on
    a model that is genuinely freeform (no properties, allOf,
    anyOf, or oneOf). Any other default value still returns
    PropertyError with the original message, so models with required
    fields can't slip through with an empty default.
  • Snapshot impact: confirmed none. Snapshot tests pass without
    pdm regen.

Related

Companion PR: #1448 — a second independent fix for the
title-collision case surfaced from the same downstream spec. The two
PRs share no code paths and can land in either order.

A schema declared as `type: object` with `additionalProperties: true` and no
declared properties (often appearing inside an `anyOf` with `null`) could not
carry a `default: {}`. `ModelProperty.convert_value` rejected the default with
`ModelProperty cannot have a default value`, which propagated up as a warning
and silently dropped the enclosing schema and every endpoint referencing it.

`ModelProperty.convert_value` now accepts the empty dict on a freeform model
and emits a `ClassName()` constructor call. When `required_properties` has not
yet been populated, the check falls back to the raw schema (`data.properties`,
`data.allOf`, `data.anyOf`, `data.oneOf`) so referenced schemas not yet
processed still get the correct decision.

A default value also forces the generated client to import the inner model at
runtime rather than only under `TYPE_CHECKING`, otherwise the default
initializer would `NameError` at class-definition time. `get_imports` and
`get_lazy_imports` are updated accordingly on both `ModelProperty` and
`UnionProperty` so inner-property imports are promoted when the property has a
non-`None` default.

Tests: a functional test in `end_to_end_tests/functional_tests` covers the
inline spec, and unit tests in
`tests/test_parser/test_properties/test_model_property.py` cover the new
branches in `convert_value`, `get_imports`, and `get_lazy_imports`.
alexander-wenzel-dev added a commit to alexander-wenzel-dev/mealie-mcp that referenced this pull request Jun 6, 2026
Adds a [tool.uv.sources] override resolving openapi-python-client from
alexander-wenzel-dev/openapi-python-client at branch
pin/mealie-mcp-combined. The branch carries two unmerged generator
fixes (upstream PRs openapi-generators/openapi-python-client#1448 and
openapi-generators/openapi-python-client#1449) that unblock the
Recipe-Input / Recipe-Output models and the PUT/PATCH recipe endpoints
in the generated client.

This commit only changes resolution; the generated client tree is
untouched and gets regenerated in a separate PR. Removal plan is
inline in pyproject.toml.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant