datamodel-code-generator icon indicating copy to clipboard operation
datamodel-code-generator copied to clipboard

Improve `mypy` hinting on `RootModel` with `oneOf`

Open woodruffw opened this issue 10 months ago • 4 comments

First of all, thank you for this project! It's been invaluable in a few of my libraries.

Is your feature request related to a problem? Please describe.

Per https://github.com/koxudaxi/datamodel-code-generator/issues/1779 and similar issues, datamodel-codegen currently produces unified oneOf types using RootModel as a base class, e.g.:

class Foo(RootModel[Union[Bar, Baz]]):
    root: Union[Bar, Baz]

This is semantically correct and supported by Pydantic. However, as noted in https://github.com/pydantic/pydantic/discussions/7418, some typecheckers (like mypy) struggle with this format.

For Python 3.8+, Pydantic recommends this form instead:

Foo = RootModel[Union[Bar, Baz]]

...which mypy does understand, and is able to infer Foo(...)'s params/kwargs for.

Describe the solution you'd like

When --target-python-version is 3.8 or newer, datamodel-codegen should emit (or be able to be configured to emit) the RootModel[...] annotation format, rather than the inheritance format.

Describe alternatives you've considered

I could just bushwack around this in my codebases by ignoring the mypy errors. But it'd be nice to have a generalized fix upstream here 😅

Additional context

N/A

woodruffw avatar Apr 05 '24 18:04 woodruffw

For a concrete example of this failing, see this CI job.

This happens because of a generated type that looks like this:

class DsseSchema(RootModel[Union[DsseV001Schema1, DsseV001Schema2]]):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    root: Union[DsseV001Schema1, DsseV001Schema2] = Field(
        ...,
        description="log entry schema for dsse envelopes",
        title="DSSE Schema",
    )

...which then gets instantiated like this:

proposed_entry = rekor_types.Dsse(
    spec=rekor_types.dsse.DsseSchema(
        proposed_content=rekor_types.dsse.ProposedContent(
            envelope=content.to_json(),
            verifiers=[b64_cert.decode()],
        ),
    ),
)

...which fails, since Dsse's constructor is not inferred to be the superset of DsseV001Schema1 | DsseV001Schema2.

woodruffw avatar Apr 05 '24 18:04 woodruffw

@woodruffw Thank you for exploring the solution. Is your error related to the issue? https://github.com/koxudaxi/datamodel-code-generator/issues/1921

I would like to know, if you know, if your solution is version-constrained on the Pydantic side? Can it be used with 2.5, etc.?

koxudaxi avatar Apr 25 '24 15:04 koxudaxi

Thanks for your response @koxudaxi! #1921 looks like a different issue -- in my case the generated code is syntactically and semantically sound, but just doesn't work with the patterns mypy and Pydantic both understand for kwarg inference 🙂

I would like to know, if you know, if your solution is version-constrained on the Pydantic side? Can it be used with 2.5, etc.?

Yes, I believe it can be used with anything in 2.x!

woodruffw avatar Apr 25 '24 17:04 woodruffw

Thanks for the bug report @woodruffw - I am also running into this issue.

JakeSummers avatar May 03 '24 00:05 JakeSummers