Repository: koxudaxi/datamodel-code-generator

Issue:
$ref doesn't work with extra fields
**Describe the bug**
$ref doesn't work when used with other fields.

**To Reproduce**

Example schema:
```json

{
  "definitions": {
    "ipv4-addr": {
      "description": "IPv4 address",
      "markdownDescription": "IPv4 address",
      "type": "string",
      "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(%[\\p{N}\\p{L}]+)?$"
    },
    "ipv6-addr": {
      "description": "IPv6 address",
      "markdownDescription": "IPv6 address",
      "type": "string",
      "pattern": "^((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))(%[\\p{N}\\p{L}]+)?$"
    }
  },
  "type": "object",
  "properties": {
    "ipv4-gw": {
      "$ref": "#/definitions/ipv4-addr"
    },
    "ipv6-gw": {
      "description": "IPv6 gateway address that will be set on a bridge used for the management network. Will be set to the first available IP address by default",
      "markdownDescription": "IPv6 gateway address that will be set on a bridge used for the management network. Will be set to the first available IP address by default",
      "$ref": "#/definitions/ipv6-addr"
    }
  },
  "additionalProperties": false
}

```

Used commandline:
```
$ uvx --from datamodel-code-generator datamodel-codegen --input schema.json --input-file-type jsonschema --output models.py --output-model-type pydantic_v2.BaseModel --use-annotated --formatters ruff-format ruff-check
```

**Expected behavior**
`ipv4-gw` here has no other fields than `$ref`, which got converted into `ipv4_gw: Annotated[Ipv4Addr | None, Field(alias="ipv4-gw")] = None`.

`ipv6-gw`, on the other hand, got converted into 
```
    ipv6_gw: Annotated[
        str | None,
        Field(
            alias="ipv6-gw",
            description="IPv6 gateway address that will be set on a bridge used for the management network. Will be set to the first available IP address by default",
        ),
    ] = None
```
when it should've generated `Ipv6Addr | None` or at least preserve the pattern.

**Version:**
 - OS: Linux
 - Python version: 3.13
 - datamodel-code-generator version: v0.42.2 - v0.53.0

**Additional context**
This bug was introduced in v0.42.2; v0.42.1 works fine. In addition, when run without `--use-annotated` it generates constr with pattern.

Discussion:

Fix (patch):
diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py
index e72f6bfc4..20be94d13 100644
--- a/src/datamodel_code_generator/parser/jsonschema.py
+++ b/src/datamodel_code_generator/parser/jsonschema.py
@@ -268,6 +268,10 @@ def model_rebuild(cls) -> None:
         "dynamicAnchor",
     }
 
+    __schema_affecting_extras__: set[str] = {  # noqa: RUF012
+        "const",
+    }
+
     @model_validator(mode="before")
     def validate_exclusive_maximum_and_exclusive_minimum(cls, values: Any) -> Any:  # noqa: N805
         """Validate and convert boolean exclusive maximum and minimum to numeric values."""
@@ -489,10 +493,7 @@ def has_ref_with_schema_keywords(self) -> bool:
         other_fields = get_fields_set(self) - {"ref"}
         schema_affecting_fields = other_fields - self.__metadata_only_fields__ - {"extras"}
         if self.extras:
-            # Filter out metadata-only fields AND extension fields (x-* prefix)
-            schema_affecting_extras = {
-                k for k in self.extras if k not in self.__metadata_only_fields__ and not k.startswith("x-")
-            }
+            schema_affecting_extras = {k for k in self.extras if k in self.__schema_affecting_extras__}
             if schema_affecting_extras:
                 schema_affecting_fields |= {"extras"}
         return bool(schema_affecting_fields)
@@ -511,9 +512,7 @@ def is_ref_with_nullable_only(self) -> bool:
         if other_fields:
             return False
         if self.extras:
-            schema_affecting_extras = {
-                k for k in self.extras if k not in self.__metadata_only_fields__ and not k.startswith("x-")
-            }
+            schema_affecting_extras = {k for k in self.extras if k in self.__schema_affecting_extras__}
             if schema_affecting_extras:
                 return False
         return True


Test changes:
diff --git a/tests/data/expected/main/jsonschema/ref_nullable_with_extra.py b/tests/data/expected/main/jsonschema/ref_nullable_with_extra.py
index dd829a90c..7aea245e3 100644
--- a/tests/data/expected/main/jsonschema/ref_nullable_with_extra.py
+++ b/tests/data/expected/main/jsonschema/ref_nullable_with_extra.py
@@ -7,13 +7,9 @@
 from pydantic import BaseModel
 
 
-class UserWithExtra(BaseModel):
+class User(BaseModel):
     name: str | None = None
 
 
 class Model(BaseModel):
-    user_with_extra: UserWithExtra | None = None
-
-
-class User(BaseModel):
-    name: str | None = None
+    user_with_extra: User | None = None
diff --git a/tests/data/expected/main/jsonschema/ref_nullable_with_nonstandard_metadata.py b/tests/data/expected/main/jsonschema/ref_nullable_with_nonstandard_metadata.py
new file mode 100644
index 000000000..88cc23d92
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/ref_nullable_with_nonstandard_metadata.py
@@ -0,0 +1,15 @@
+# generated by datamodel-codegen:
+#   filename:  ref_nullable_with_nonstandard_metadata.yaml
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from pydantic import BaseModel
+
+
+class User(BaseModel):
+    name: str | None = None
+
+
+class Model(BaseModel):
+    user: User | None = None
diff --git a/tests/data/expected/main/jsonschema/ref_with_const.py b/tests/data/expected/main/jsonschema/ref_with_const.py
new file mode 100644
index 000000000..7c0a8285a
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/ref_with_const.py
@@ -0,0 +1,25 @@
+# generated by datamodel-codegen:
+#   filename:  ref_with_const.json
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from enum import Enum
+from typing import Literal
+
+from pydantic import BaseModel
+
+
+class Status(Enum):
+    active = 'active'
+    inactive = 'inactive'
+
+
+class NullableStatus(Enum):
+    active = 'active'
+    inactive = 'inactive'
+
+
+class Model(BaseModel):
+    status: Literal['active']
+    nullable_status: Literal['active'] = 'active'
diff --git a/tests/data/expected/main/jsonschema/ref_with_nonstandard_metadata.py b/tests/data/expected/main/jsonschema/ref_with_nonstandard_metadata.py
new file mode 100644
index 000000000..5abd48179
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/ref_with_nonstandard_metadata.py
@@ -0,0 +1,18 @@
+# generated by datamodel-codegen:
+#   filename:  ref_with_nonstandard_metadata.json
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from ipaddress import IPv6Address
+
+from pydantic import BaseModel, RootModel
+
+
+class Ipv6Addr(RootModel[IPv6Address]):
+    root: IPv6Address
+
+
+class Model(BaseModel):
+    ipv6_address: Ipv6Addr
+    ipv6_address_nullable: Ipv6Addr | None = None
diff --git a/tests/data/jsonschema/ref_nullable_with_nonstandard_metadata.yaml b/tests/data/jsonschema/ref_nullable_with_nonstandard_metadata.yaml
new file mode 100644
index 000000000..fb172c377
--- /dev/null
+++ b/tests/data/jsonschema/ref_nullable_with_nonstandard_metadata.yaml
@@ -0,0 +1,13 @@
+$schema: "http://json-schema.org/draft-07/schema#"
+definitions:
+  User:
+    type: object
+    properties:
+      name:
+        type: string
+type: object
+properties:
+  user:
+    $ref: "#/definitions/User"
+    nullable: true
+    markdownDescription: "A user object"
diff --git a/tests/data/jsonschema/ref_with_const.json b/tests/data/jsonschema/ref_with_const.json
new file mode 100644
index 000000000..c14eb184e
--- /dev/null
+++ b/tests/data/jsonschema/ref_with_const.json
@@ -0,0 +1,22 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "definitions": {
+    "Status": {
+      "type": "string",
+      "enum": ["active", "inactive"]
+    }
+  },
+  "type": "object",
+  "properties": {
+    "status": {
+      "$ref": "#/definitions/Status",
+      "const": "active"
+    },
+    "nullable_status": {
+      "$ref": "#/definitions/Status",
+      "nullable": true,
+      "const": "active"
+    }
+  },
+  "required": ["status"]
+}
diff --git a/tests/data/jsonschema/ref_with_nonstandard_metadata.json b/tests/data/jsonschema/ref_with_nonstandard_metadata.json
new file mode 100644
index 000000000..420a2e867
--- /dev/null
+++ b/tests/data/jsonschema/ref_with_nonstandard_metadata.json
@@ -0,0 +1,28 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "definitions": {
+    "Ipv6Addr": {
+      "type": "string",
+      "format": "ipv6"
+    }
+  },
+  "type": "object",
+  "properties": {
+    "ipv6_address": {
+      "$ref": "#/definitions/Ipv6Addr",
+      "markdownDescription": "An IPv6 address"
+    },
+    "ipv6_address_nullable": {
+      "anyOf": [
+        {
+          "$ref": "#/definitions/Ipv6Addr"
+        },
+        {
+          "type": "null"
+        }
+      ],
+      "markdownDescription": "An optional IPv6 address"
+    }
+  },
+  "required": ["ipv6_address"]
+}
diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py
index d2bb5ad85..a17da988d 100644
--- a/tests/main/jsonschema/test_main_jsonschema.py
+++ b/tests/main/jsonschema/test_main_jsonschema.py
@@ -7912,11 +7912,12 @@ def test_ref_nullable_with_constraint_creates_model(output_file: Path) -> None:
     )
 
 
-def test_ref_nullable_with_extra_creates_model(output_file: Path) -> None:
-    """Test $ref + nullable: true + schema-affecting extras DOES create a merged model.
+def test_ref_nullable_with_extra_uses_reference_directly(output_file: Path) -> None:
+    """Test $ref + nullable: true + non-schema-affecting extras uses reference directly.
 
-    When a property has $ref with nullable: true AND schema-affecting extras like
-    'if', 'then', 'else', it should merge the schemas and create a new model.
+    When a property has $ref with nullable: true AND extras that the tool cannot
+    structurally process (like 'if'), it should use the reference directly
+    instead of creating a merged model.
     """
     run_main_and_assert(
         input_path=JSON_SCHEMA_DATA_PATH / "ref_nullable_with_extra.yaml",
@@ -8539,3 +8540,54 @@ def test_main_jsonschema_multiple_aliases_required_pydantic_v2(output_file: Path
             "pydantic_v2.BaseModel",
         ],
     )
+
+
+def test_ref_with_nonstandard_metadata(output_file: Path) -> None:
+    """Test $ref with non-standard metadata fields preserves type information.
+
+    When $ref is combined with non-standard metadata like 'markdownDescription',
+    the reference type should be preserved instead of being replaced by the
+    underlying type. Non-standard fields are annotation-only and should not
+    trigger schema merging.
+    """
+    run_main_and_assert(
+        input_path=JSON_SCHEMA_DATA_PATH / "ref_with_nonstandard_metadata.json",
+        output_path=output_file,
+        input_file_type="jsonschema",
+        assert_func=assert_file_content,
+        expected_file="ref_with_nonstandard_metadata.py",
+        extra_args=["--output-model-type", "pydantic_v2.BaseModel", "--use-annotated"],
+    )
+
+
+def test_ref_nullable_with_nonstandard_metadata(output_file: Path) -> None:
+    """Test $ref + nullable: true with non-standard metadata uses reference directly.
+
+    When $ref is combined with nullable: true and non-standard metadata like
+    'markdownDescription', the reference should be used directly with Optional
+    type annotation instead of creating a merged model.
+    """
+    run_main_and_assert(
+        input_path=JSON_SCHEMA_DATA_PATH / "ref_nullable_with_nonstandard_metadata.yaml",
+        output_path=output_file,
+        input_file_type="jsonschema",
+        assert_func=assert_file_content,
+        expected_file="ref_nullable_with_nonstandard_metadata.py",
+        extra_args=["--output-model-type", "pydantic_v2.BaseModel", "--strict-nullable"],
+    )
+
+
+def test_ref_with_const(output_file: Path) -> None:
+    """Test $ref + const triggers schema merging as const is schema-affecting.
+
+    When $ref is combined with 'const', the const keyword structurally affects
+    the generated type (producing Literal), so schema merging should occur.
+    """
+    run_main_and_assert(
+        input_path=JSON_SCHEMA_DATA_PATH / "ref_with_const.json",
+        output_path=output_file,
+        input_file_type="jsonschema",
+        assert_func=assert_file_content,
+        expected_file="ref_with_const.py",
+        extra_args=["--output-model-type", "pydantic_v2.BaseModel", "--strict-nullable"],
+    )