Skip to content

Commit 3220f8a

Browse files
committed
Fix duplicate model generation for $ref with nullable
1 parent 1d221da commit 3220f8a

11 files changed

Lines changed: 262 additions & 1 deletion

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,27 @@ def has_ref_with_schema_keywords(self) -> bool:
477477
schema_affecting_fields |= {"extras"}
478478
return bool(schema_affecting_fields)
479479

480+
@cached_property
481+
def is_ref_with_nullable_only(self) -> bool:
482+
"""Check if schema has $ref with only nullable: true (no other schema-affecting keywords).
483+
484+
This is used to avoid creating duplicate models when a $ref is combined
485+
with nullable: true. In such cases, the reference should be used directly
486+
with Optional type annotation instead of merging schemas.
487+
"""
488+
if not self.ref or self.nullable is not True:
489+
return False
490+
other_fields = get_fields_set(self) - {"ref", "nullable"} - self.__metadata_only_fields__ - {"extras"}
491+
if other_fields:
492+
return False
493+
if self.extras:
494+
schema_affecting_extras = {
495+
k for k in self.extras if k not in self.__metadata_only_fields__ and not k.startswith("x-")
496+
}
497+
if schema_affecting_extras:
498+
return False
499+
return True
500+
480501

481502
@lru_cache
482503
def get_ref_type(ref: str) -> JSONReference:
@@ -2700,6 +2721,12 @@ def parse_item( # noqa: PLR0911, PLR0912, PLR0914
27002721
item,
27012722
root_type_path,
27022723
)
2724+
# Handle $ref + nullable-only case without merging to avoid duplicate models
2725+
if item.is_ref_with_nullable_only and item.ref:
2726+
ref_data_type = self.get_ref_data_type(item.ref)
2727+
if self.strict_nullable:
2728+
return self.data_type(data_types=[ref_data_type], is_optional=True)
2729+
return ref_data_type
27032730
if item.has_ref_with_schema_keywords:
27042731
item = self._merge_ref_with_schema(item)
27052732
if item.ref:
@@ -3523,7 +3550,8 @@ def parse_obj( # noqa: PLR0912
35233550
path: list[str],
35243551
) -> None:
35253552
"""Parse a JsonSchemaObject by dispatching to appropriate parse methods."""
3526-
if obj.has_ref_with_schema_keywords:
3553+
# Skip merge for $ref + nullable-only (handled by ref resolution in parse_item)
3554+
if obj.has_ref_with_schema_keywords and not obj.is_ref_with_nullable_only:
35273555
obj = self._merge_ref_with_schema(obj)
35283556

35293557
if obj.is_array:
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# generated by datamodel-codegen:
2+
# filename: ref_nullable_only.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic import BaseModel
8+
9+
10+
class User(BaseModel):
11+
name: str
12+
13+
14+
class Model(BaseModel):
15+
user_a: User | None = None
16+
user_b: User | None = None
17+
user_c: User | None = None
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# generated by datamodel-codegen:
2+
# filename: ref_nullable_only.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic import BaseModel
8+
9+
10+
class User(BaseModel):
11+
name: str
12+
13+
14+
class Model(BaseModel):
15+
user_a: User | None = None
16+
user_b: User | None = None
17+
user_c: User | None = None
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# generated by datamodel-codegen:
2+
# filename: ref_nullable_with_constraint.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic import BaseModel, RootModel, constr
8+
9+
10+
class Model(BaseModel):
11+
constrained_string: constr(min_length=5) | None = None
12+
13+
14+
class StringType(RootModel[str]):
15+
root: str
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# generated by datamodel-codegen:
2+
# filename: ref_nullable_with_extra.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic import BaseModel
8+
9+
10+
class UserWithExtra(BaseModel):
11+
name: str | None = None
12+
13+
14+
class Model(BaseModel):
15+
user_with_extra: UserWithExtra | None = None
16+
17+
18+
class User(BaseModel):
19+
name: str | None = None
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# generated by datamodel-codegen:
2+
# filename: ref_nullable_with_metadata.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic import BaseModel, Field
8+
9+
10+
class User(BaseModel):
11+
name: str
12+
13+
14+
class Model(BaseModel):
15+
user_with_title: User | None = Field(
16+
None,
17+
description='A user reference with additional metadata',
18+
title='User with Title',
19+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
$schema: "http://json-schema.org/draft-07/schema#"
2+
definitions:
3+
User:
4+
type: object
5+
properties:
6+
name:
7+
type: string
8+
required:
9+
- name
10+
type: object
11+
properties:
12+
user_a:
13+
$ref: "#/definitions/User"
14+
nullable: true
15+
user_b:
16+
$ref: "#/definitions/User"
17+
nullable: true
18+
user_c:
19+
$ref: "#/definitions/User"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
$schema: "http://json-schema.org/draft-07/schema#"
2+
definitions:
3+
StringType:
4+
type: string
5+
type: object
6+
properties:
7+
constrained_string:
8+
$ref: "#/definitions/StringType"
9+
nullable: true
10+
minLength: 5
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
$schema: "http://json-schema.org/draft-07/schema#"
2+
definitions:
3+
User:
4+
type: object
5+
properties:
6+
name:
7+
type: string
8+
type: object
9+
properties:
10+
user_with_extra:
11+
$ref: "#/definitions/User"
12+
nullable: true
13+
if:
14+
properties:
15+
name:
16+
const: admin
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
$schema: "http://json-schema.org/draft-07/schema#"
2+
definitions:
3+
User:
4+
type: object
5+
properties:
6+
name:
7+
type: string
8+
required:
9+
- name
10+
type: object
11+
properties:
12+
user_with_title:
13+
$ref: "#/definitions/User"
14+
nullable: true
15+
title: User with Title
16+
description: A user reference with additional metadata

0 commit comments

Comments
 (0)