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

allOf {a, b.b1, b.b2} breaks model

Open TimmFitschen opened this issue 5 months ago • 1 comments

Describe the bug

As part of a much bigger API where I needed to use "allOf" to construct a schema in a DRY way I encountered the following problem:

When you have a schema with "allOf" using three references with a special inner structure the generator produces a broken model.

The structure is

"allOf": [
  { "a": {}},
  { "b": { "b1": {}},
  { "b": { "b2": {}},
]

The broken model looks like this:

...
class BB1(BaseModel):
    b: Optional[B1] = None

class BB2(BaseModel):
    b: Optional[B2] = None

class Model(A, BB1, BB2):
    pass

So, you cannot have both b.b1 and b.b2 in the data.

To Reproduce

Find everything at https://github.com/TimmFitschen/datamode-code-generator-issues-2413

Example schema (api.json):

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "allOf": [
    { "$ref": "#/$defs/Data" },
    { "$ref": "#/$defs/SelfLink" },
    { "$ref": "#/$defs/CollectionLink"}
  ],
  "$defs": {
    "Data": {
      "properties": {
        "data": {
          "type": "string"
        }      }    },
    "SelfLink": {
      "properties": {
        "links": {
          "properties": {
            "self": {
              "type": "string"
            }          }        }      }    },
    "CollectionLink": {
      "properties": {
        "links": {
          "properties": {
            "collection": {
              "type": "string"
            }          }        }      }    }  }
}

Used commandline:

$ datamodel-codegen  --input api.json --output model.py

The model (model.py):

from __future__ import annotations
from typing import Optional
from pydantic import BaseModel

class Data(BaseModel):
    data: Optional[str] = None

class Links(BaseModel):
    self: Optional[str] = None

class SelfLink(BaseModel):
    links: Optional[Links] = None

class Links1(BaseModel):
    collection: Optional[str] = None

class CollectionLink(BaseModel):
    links: Optional[Links1] = None

class Model(Data, SelfLink, CollectionLink):
    pass

Expected behavior

My failing test:

from model import Model

test_data = {
 "data": "test",
 "links": {
   "self": "https://localhost:8000/data/test",
   "collection": "https://localhost:8000/data"
 }
}

def test_instanciation():
    model = Model(**test_data)
    assert model.links.self is not None
    assert model.links.collection is not None

Version:

  • OS: Linux
  • Python version: 3.12
  • datamodel-code-generator version: 0.30.2

Additional context

I was able to find a workaround which is ok for me and might give you an idea what it going wrong. I added another option to the "allOf" which is meaningless but triggers a different model generation:

...
  "allOf": [
    { "$ref": "#/$defs/Data" },
    { "$ref": "#/$defs/SelfLink" },
    { "$ref": "#/$defs/CollectionLink"},
    { "properties": {
        "links": {
          "type": "object"
        }
      }
    }
  ],
...

Now my model has

...

class Workaround(Data, SelfLink, CollectionLink):
    links: Optional[Dict[str, Any]] = None
...

and the following test is passing:

...
def test_workaround():
    model = Workaround(**test_data)
    print(model.model_dump())
    assert model.links["self"] is not None
    assert model.links["collection"] is not None

Thank you for your effort! I hope this helps.

TimmFitschen avatar Jun 07 '25 11:06 TimmFitschen

See code: https://github.com/TimmFitschen/datamode-code-generator-issues-2413

TimmFitschen avatar Jun 07 '25 11:06 TimmFitschen