Skip to content

Fix OpenAPI default, const, and $ref parameter support#247

Merged
phalt merged 5 commits into
phalt:mainfrom
chassing:2026-06-03_openapi-defaults
Jun 5, 2026
Merged

Fix OpenAPI default, const, and $ref parameter support#247
phalt merged 5 commits into
phalt:mainfrom
chassing:2026-06-03_openapi-defaults

Conversation

@chassing

@chassing chassing commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • default support: generate_class_properties now reads "default" from OpenAPI property definitions and renders the value as a Python literal via repr(). Fields with both a default and an alias use pydantic.Field(default=..., alias="..."). Uses "default" in arg_details (not truthiness) so falsy defaults like [], 0, False, "", and null are handled correctly.
  • const support: get_type now checks for "const" and generates typing.Literal[<value>]. This is required for Pydantic discriminated unions to work — without Literal, Pydantic raises PydanticUserError: Model needs field to be of type Literal.
  • $ref in parameters: Parameters referencing schemas via $ref (e.g. enums like ActionStatus) now resolve to schemas.X in client.py instead of bare forward references with no import.
  • Double-nullable fix: anyOf: [$ref, {type: null}] parameters no longer produce X | None | None — existing nullability is detected and not duplicated.
  • schema_to_dict compat layer: Extracts both "default" and "const" from cicerone's __pydantic_extra__, which were previously silently dropped during spec parsing.

Before:

# schemas.py — defaults ignored, const ignored
class Config(pydantic.BaseModel):
    tags: list[str]       # spec says default: []
    count: int            # spec says default: 0

class NotificationAddUser(pydantic.BaseModel):
    action: str           # spec says const: "add-user" — breaks discriminated unions
    message: str

# client.py — broken import, double None
def action_list(
    status: ActionStatus | None | None = None,  # NameError: ActionStatus not imported
) -> schemas.ResponseActionList: ...

After:

# schemas.py
class Config(pydantic.BaseModel):
    tags: list[str] = []
    count: int = 0

class NotificationAddUser(pydantic.BaseModel):
    action: typing.Literal["add-user"] = "add-user"  # discriminated unions work
    message: str

# client.py
def action_list(
    status: typing.Optional[schemas.ActionStatus] = None,  # correct import, single None
) -> schemas.ResponseActionList: ...

Test plan

  • test_generate_class_properties_with_defaults — list [], int 0, bool False, string "", null defaults
  • test_generate_class_properties_default_with_alias — field with alias + default uses pydantic.Field
  • test_generate_class_properties_no_required_array_with_defaults — schema with no required array, all fields have explicit defaults
  • test_generate_class_properties_with_const — const generates typing.Literal
  • test_generate_class_properties_const_with_default — const + default generates Literal type with default value
  • test_generate_class_properties_const_with_alias — const + alias uses pydantic.Field
  • test_resolve_forward_refs_for_client — forward refs replaced with schemas.X
  • test_strip_none_from_type — handles Optional, Union with None, pipe None, plain types
  • test_clients_generator_resolves_schema_refs_in_parametersanyOf: [$ref, null] produces typing.Optional[schemas.X] without double None
  • End-to-end: specs with discriminator + enum ref parameters generate correct, loadable code
  • Full test suite passes (559/559, excluding pre-existing aiohttp failures)

@chassing chassing marked this pull request as draft June 3, 2026 10:51
@chassing chassing marked this pull request as ready for review June 3, 2026 10:55
@chassing chassing marked this pull request as draft June 3, 2026 11:20
@chassing chassing marked this pull request as ready for review June 3, 2026 11:25
@chassing chassing changed the title Fix generate_class_properties ignoring OpenAPI default values Fix OpenAPI default and const support in schema generation Jun 3, 2026
@chassing chassing changed the title Fix OpenAPI default and const support in schema generation Fix OpenAPI default, const, and $ref parameter support Jun 3, 2026
@phalt

phalt commented Jun 4, 2026

Copy link
Copy Markdown
Owner

@chassing thanks for this, I am going to try and look into it tomorrow (Friday) and get this merged and shipped. Please could you update the CHANGELOG.md files with one-sentence notes of the fixes? Feel free to make a new version 2.2.0

chassing added 5 commits June 5, 2026 08:37
Properties with explicit `"default"` values in OpenAPI specs (e.g. `[]`,
`0`, `true`, `false`, `null`) were generated as required fields without
defaults. Now checks for `"default"` in property details and renders
the value using `repr()` for correct Python literals. Fields needing
both an alias and a default use `pydantic.Field(default=..., alias="...")`.
The `schema_to_dict` compat layer was not extracting `"default"` from
cicerone's `__pydantic_extra__`, so defaults from the OpenAPI spec
were lost before reaching `generate_class_properties`. Uses `"in"`
membership check instead of truthiness since defaults can be falsy
(`[]`, `0`, `false`, `null`).
`schema_to_dict` now extracts `"const"` from cicerone's
`__pydantic_extra__`. `get_type` returns `typing.Literal[<value>]`
when `"const"` is present, which is required for Pydantic
discriminated unions to work correctly.
Parameters referencing schemas via `$ref` (e.g. enums like
`ActionStatus`) produced bare forward references in `client.py`
with no import. Now `generate_parameters` resolves forward refs
to `schemas.X` via `resolve_forward_refs_for_client`.

Also fixes double-nullable types (`X | None | None`) when
`anyOf: [$ref, null]` was wrapped again in `typing.Optional`.
`strip_none_from_type` removes existing None before re-wrapping.
@phalt phalt force-pushed the 2026-06-03_openapi-defaults branch from aa3f793 to 24db8a2 Compare June 5, 2026 07:37
@phalt

phalt commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Rebasing so CI can run.

@phalt phalt left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. I am working to fix a few more bugs on another issue - I think you actually fix one of mine here! So I'll merge your stuff and then continue with my work before I do the final 2.2.0 release :)

@phalt phalt merged commit 6b92cfb into phalt:main Jun 5, 2026
5 checks passed
phalt pushed a commit that referenced this pull request Jun 5, 2026
Bug 1 (enum forward refs in client.py) was resolved by PR #247 via
resolve_forward_refs_for_client().

Two bugs remain unaddressed:

Bug 2 - Array response schemas are emitted as bare type aliases
  (e.g. UploadFaces200Response = list[FaceItem]).  A GenericAlias is
  not a type, so pydantic rejects it in response_map: dict[int, type[Any]].
  Fix should emit a pydantic.RootModel subclass instead.

Bug 3 - Generated config.py example says API_BASE_URL but pydantic-settings
  reads BASE_URL (the field is named base_url).  The wrong name is silently
  ignored (extra='ignore') and the client falls back to http://localhost.
  Fix should document BASE_URL and base_url= in the example.

https://claude.ai/code/session_011e1xPCeW7nD8rn9vWppPPm
phalt pushed a commit that referenced this pull request Jun 5, 2026
Bug 1 (enum forward refs in client.py) was resolved by PR #247 via
resolve_forward_refs_for_client().

Two bugs remain unaddressed:

Bug 2 - Array response schemas are emitted as bare type aliases
  (e.g. UploadFaces200Response = list[FaceItem]).  A GenericAlias is
  not a type, so pydantic rejects it in response_map: dict[int, type[Any]].
  Fix should emit a pydantic.RootModel subclass instead.

Bug 3 - Generated config.py example says API_BASE_URL but pydantic-settings
  reads BASE_URL (the field is named base_url).  The wrong name is silently
  ignored (extra='ignore') and the client falls back to http://localhost.
  Fix should document BASE_URL and base_url= in the example.

https://claude.ai/code/session_011e1xPCeW7nD8rn9vWppPPm
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.

2 participants