koxudaxi/datamodel-code-generator

[one|any|all]Of+required schema rules produce numerous near-identical models which are then quite hard to differentiate

fiendish opened this issue · 0 comments

Describe the bug
Combining [one|all|any]Of + required generates excessive near-identical models instead of treating them as root validation rules. This makes the output relatively quite verbose and significantly more difficult to debug.

To Reproduce

Example schema:

type: object
properties:
  a:
    type: string
  b:
    type: string
  c:
    type: string
  d:
    type: string
anyOf:
  - required:
    - a
  - required:
    - b

Used commandline:

$ datamodel-codegen --reuse-model --output-model-type pydantic_v2.BaseModel --target-python-version 3.9 --use-subclass-enum --collapse-root-models --allow-extra-fields --input openapi/components/schemas/test.yaml --output test.py 

Generated output:

from __future__ import annotations

from typing import Optional, Union

from pydantic import BaseModel, ConfigDict, RootModel


class Model1(BaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    a: str
    b: Optional[str] = None
    c: Optional[str] = None
    d: Optional[str] = None


class Model2(BaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    a: Optional[str] = None
    b: str
    c: Optional[str] = None
    d: Optional[str] = None


class Model(RootModel[Union[Model1, Model2]]):
    root: Union[Model1, Model2]

Expected behavior

I believe it would be better in cases like this if the output were as follows with the rules recorded as rules instead of model clones

from pydantic import BaseModel, root_validator, ValidationError
from typing import Optional

class Model(BaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    a: Optional[str] = None
    b: Optional[str] = None
    c: Optional[str] = None
    d: Optional[str] = None

    @root_validator(pre=True)
    def at_least_one_field(cls, values):
        if not any(values.get(field) for field in ['a', 'b']):
            raise ValueError('At least one of "a", "b" must be provided')

        return values

Keep in mind that the above is a stripped-down compact test. In practice the output is substantially more difficult to evaluate, e.g....

class Model1(BaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    a: str = Field(
        ...,
        description='...',
        examples=['...'],
    )
    b: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    c: Optional[str] = Field(
        None, description='...',
    )
    d:: Optional[str] = Field(
        None, description='...',
    )
    e: Optional[str] = Field(
        None, description='...',
    )
    f: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    g: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    h: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    i: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    j: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    k: Optional[str] = Field(
        None,
        description='...',
    )
    l: Optional[bool] = Field(
        None,
        description='...',
        examples=[False],
    )
    m: Optional[str] = Field(
        None, description='...',
    )
    n: Optional[str] = Field(
        None, description='...',
    )


class Model2(BaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    a: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    b: str = Field(
        ...,
        description='...',
        examples=['...'],
    )
    c: Optional[str] = Field(
        None, description='...',
    )
    d:: Optional[str] = Field(
        None, description='...',
    )
    e: Optional[str] = Field(
        None, description='...',
    )
    f: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    g: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    h: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    i: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    j: Optional[str] = Field(
        None,
        description='...',
        examples=['...'],
    )
    k: Optional[str] = Field(
        None,
        description='...',
    )
    l: Optional[bool] = Field(
        None,
        description='...',
        examples=[False],
    )
    m: Optional[str] = Field(
        None, description='...',
    )
    n: Optional[str] = Field(
        None, description='...',
    )

Version:

  • OS: Linux
  • Python version: 3.9.12
  • datamodel-code-generator version: 0.25.8