koxudaxi / datamodel-code-generator

Pydantic model and dataclasses.dataclass generator for easy conversion of JSON, OpenAPI, JSON Schema, and YAML data sources.
https://koxudaxi.github.io/datamodel-code-generator/
MIT License
2.76k stars 302 forks source link

Generates model containing name confusion with imported module #1456

Open syntaxaire opened 1 year ago

syntaxaire commented 1 year ago

Describe the bug Code is generated that resembles:

from datetime import date

class ActivityBase(BaseModel):
    date: Optional[date] = Field(
        None, description="The date the Activity was performed (as a ISO-8601 date)"
    )

date: Optional[date] is a recursive reference that crashes under Pydantic 2.

To Reproduce

Example schema:

{
  "openapi": "3.0.0",
  "info": {
    "title": "API Documentation",
    "contact": {
      "name": "API Support",
      "email": "api@api.com"
    },
    "description": "API documentation",
    "version": "v4"
  },
  "components": {
    "schemas": {
      "Activity_base": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date",
            "description": "The date the Activity was performed (as a ISO-8601 date)"
          }
        }
      }
    }
  }
}

Used commandline:

$ datamodel-codegen --use-standard-collections --use-schema-description --use-double-quotes --input-file-type openapi --target-python-version 3.10 --encoding utf8 --input openapi.json --output models.py

Importing models.py with Pydantic 2.1.1 will then crash with a lengthy stack trace ending in:

RecursionError: maximum recursion depth exceeded

Expected behavior The generated code could use namespaced imports to prevent conflicting with common names like 'date'. For example,

import datetime

class ActivityBase(BaseModel):
    date: Optional[datetime.date] = Field(
        None, description="The date the Activity was performed (as a ISO-8601 date)"
    )

explicitly identifies the intended reference (and does not crash).

Version:

FSchumacher commented 1 year ago

A similar problem arises, when we use the following schema

{
  "components": {
    "schemas": {
      "shared.RuntimeAudit": {
        "description": "Runtime ...",
        "properties": {
          "runtime": {
            "$ref": "#/components/schemas/runtime.RuleEffect"
          }
        }
      },
      "runtime.RuleEffect": {
        "description": "Some rule effect",
        "enum": ["block", "prevent", "alert", "disable"],
        "type": "string"
      }
    }
  }
}

That will generate a pydantic v2 model like this (the shared.py file):

from __future__ import annotations

from pydantic import BaseModel

from . import runtime

class RuntimeAudit(BaseModel):
    runtime: runtime.RuleEffect | None = None

Importing this module will raise an AttributeError because pydantic v2 (and probably python) will be confused by the field having the same name as the module.

AttributeError: 'NoneType' object has no attribute 'RuleEffect'

Would it be possible to use an alias for the module, if a naming conflict is detected? For example

from . import runtime as rt

class RuntimeAudit(BaseModel):
    runtime: rt.RuleEffect | None = None
joakimnordling commented 10 months ago

Any update on if there's any plan to fix this bug?

We are having the exact same issue with a field named date and having the format set to "date". We're using it in openapi-to-fastapi which generates FastAPI routes on the fly from OpenAPI specs, which means we don't really have the option to manually edit the outputted pydantic models.

I guess I'll need to see if I can figure out some work around or if I'm able to somehow fix the issue in datamodel-code-generator myself. If anyone has any good suggestions on any work around (that does not involve renaming the field and work dynamically for files with arbitrary names etc) I'd be thankful. Or if there's any pointers to how it could be fixed in datamodel-code-generator that could also be helpful.

iain-palmer commented 9 months ago

I'm hitting a similar issue; this is a simplified reproduction with the following input jsonschema:

foo.json

{
  "$id": "foo.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Foo",
  "type": "object",
  "properties": {
    "bar": {
      "$ref": "bar.json"
    }
  }
}

bar.json

{
  "$id": "bar.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Bar",
  "type": "object"
}

I ran this to generate pydantic models:

datamodel-codegen --input mydir/ --output-model-type pydantic_v2.BaseModel --output mydir/

This generates the following:

foo.py

from __future__ import annotations

from typing import Optional

from pydantic import BaseModel

from . import bar

class Foo(BaseModel):
    bar: Optional[bar.Bar] = None

bar.py

from __future__ import annotations

from pydantic import BaseModel

class Bar(BaseModel):
    pass

When trying to import Foo, this results in an AttributeError as above:

AttributeError: 'NoneType' object has no attribute 'Bar'

Edit

To add to this, the following Pydantic issue gives a bit more context: https://github.com/pydantic/pydantic/issues/7554

For the example above, one workaround is to use an alias:

aliases.json

{ "bar": "bar_" }

Running with the following:

datamodel-codegen --input mydir/ --output-model-type pydantic_v2.BaseModel --output mydir/ --aliases aliases.json

This then changes Foo to the following:

class Foo(BaseModel):
    bar_: Optional[bar.Bar] = Field(None, alias='bar')