Repository: koxudaxi/datamodel-code-generator

Issue:
I use --extra-fields forbid and it doesn't work
my datamodel-code-generator version is: 0.31.0

my json:
```json
{
    "code": 0,
    "message": "success",
    "data": {
        "user_id": 12345,
        "username": "test_user",
        "email": "user@example.com",
        "permissions": ["read", "write", "delete"],
        "profile": {"level": 5, "score": 85, "tags": ["active", "premium"]},
        "history": [
            {"action": "login", "timestamp": "2024-01-01"},
            {"action": "update", "timestamp": "2024-01-02", "details": {"old_value": "100", "new_value": "200"}}
        ]
    }
}
```

my command line:
```bash
datamodel-codegen --input test.json --input-file-type json --output test.py --output-model-type pydantic_v2.BaseModel --class-name Test --use-annotated --field-constraints --snake-case-field --allow-population-by-field-name --extra-fields forbid --use-default --strip-default-none --use-schema-description --use-field-description --reuse-model --use-default-kwarg --target-python-version 3.12 --encoding utf-8 --use-union-operator --use-standard-collections --disable-timestamp --formatters ruff-format ruff-check --extra-fields forbid
```

generate result is:
```python
# generated by datamodel-codegen:
#   filename:  test.json

from __future__ import annotations

from pydantic import BaseModel, ConfigDict


class Profile(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    level: int
    score: int
    tags: list[str]


class Details(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    old_value: str
    new_value: str


class HistoryItem(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    action: str
    timestamp: str
    details: Details | None


class Data(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    user_id: int
    username: str
    email: str
    permissions: list[str]
    profile: Profile
    history: list[HistoryItem]


class Test(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    code: int
    message: str
    data: Data

```

Shouldn't this be the correct result?

```python
# generated by datamodel-codegen:
#   filename:  test.json

from __future__ import annotations

from pydantic import BaseModel, ConfigDict


class Profile(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
        extra="forbid",
    )
    level: int
    score: int
    tags: list[str]


class Details(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
        extra="forbid",
    )
    old_value: str
    new_value: str


class HistoryItem(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
        extra="forbid",
    )
    action: str
    timestamp: str
    details: Details | None


class Data(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
        extra="forbid",
    )
    user_id: int
    username: str
    email: str
    permissions: list[str]
    profile: Profile
    history: list[HistoryItem]


class Test(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
        extra="forbid",
    )
    code: int
    message: str
    data: Data
```



Discussion:
@Airpy, I've opened a PR which I believe fixes this. Apologies for the oversight.



Patch:
diff --git a/src/datamodel_code_generator/model/pydantic_v2/base_model.py b/src/datamodel_code_generator/model/pydantic_v2/base_model.py
index 918ed8e77..9329268c1 100644
--- a/src/datamodel_code_generator/model/pydantic_v2/base_model.py
+++ b/src/datamodel_code_generator/model/pydantic_v2/base_model.py
@@ -232,9 +232,20 @@ def __init__(  # noqa: PLR0913
             self.extra_template_data["config"] = ConfigDict.parse_obj(config_parameters)  # pyright: ignore[reportArgumentType]
             self._additional_imports.append(IMPORT_CONFIG_DICT)
 
-    def _get_config_extra(self) -> Literal["'allow'", "'forbid'"] | None:
+    def _get_config_extra(self) -> Literal["'allow'", "'forbid'", "'ignore'"] | None:
         additional_properties = self.extra_template_data.get("additionalProperties")
         allow_extra_fields = self.extra_template_data.get("allow_extra_fields")
-        if additional_properties is not None or allow_extra_fields:
-            return "'allow'" if additional_properties or allow_extra_fields else "'forbid'"
-        return None
+        extra_fields = self.extra_template_data.get("extra_fields")
+
+        config_extra = None
+        if allow_extra_fields or extra_fields == "allow":
+            config_extra = "'allow'"
+        elif extra_fields == "forbid":
+            config_extra = "'forbid'"
+        elif extra_fields == "ignore":
+            config_extra = "'ignore'"
+        elif additional_properties is True:
+            config_extra = "'allow'"
+        elif additional_properties is False:
+            config_extra = "'forbid'"
+        return config_extra


Test patch:
diff --git a/tests/data/expected/main/jsonschema/extra_fields_allow.py b/tests/data/expected/main/jsonschema/extra_fields_allow.py
new file mode 100644
index 000000000..c699c8981
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/extra_fields_allow.py
@@ -0,0 +1,39 @@
+# generated by datamodel-codegen:
+#   filename:  extra_fields.json
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from typing import Optional
+
+from pydantic import BaseModel, Extra
+
+
+class Foo(BaseModel):
+    class Config:
+        extra = Extra.allow
+
+    x: Optional[int] = None
+
+
+class Bar(BaseModel):
+    class Config:
+        extra = Extra.allow
+
+    y: Optional[int] = None
+
+
+class Baz(BaseModel):
+    class Config:
+        extra = Extra.allow
+
+    z: Optional[int] = None
+
+
+class Test(BaseModel):
+    class Config:
+        extra = Extra.allow
+
+    foo: Foo
+    bar: Optional[Bar] = None
+    baz: Optional[Baz] = None
diff --git a/tests/data/expected/main/jsonschema/extra_fields_forbid.py b/tests/data/expected/main/jsonschema/extra_fields_forbid.py
new file mode 100644
index 000000000..2b9a2a46b
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/extra_fields_forbid.py
@@ -0,0 +1,39 @@
+# generated by datamodel-codegen:
+#   filename:  extra_fields.json
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from typing import Optional
+
+from pydantic import BaseModel, Extra
+
+
+class Foo(BaseModel):
+    class Config:
+        extra = Extra.forbid
+
+    x: Optional[int] = None
+
+
+class Bar(BaseModel):
+    class Config:
+        extra = Extra.forbid
+
+    y: Optional[int] = None
+
+
+class Baz(BaseModel):
+    class Config:
+        extra = Extra.forbid
+
+    z: Optional[int] = None
+
+
+class Test(BaseModel):
+    class Config:
+        extra = Extra.forbid
+
+    foo: Foo
+    bar: Optional[Bar] = None
+    baz: Optional[Baz] = None
diff --git a/tests/data/expected/main/jsonschema/extra_fields_ignore.py b/tests/data/expected/main/jsonschema/extra_fields_ignore.py
index 9b7acba42..99c882982 100644
--- a/tests/data/expected/main/jsonschema/extra_fields_ignore.py
+++ b/tests/data/expected/main/jsonschema/extra_fields_ignore.py
@@ -1,5 +1,5 @@
 # generated by datamodel-codegen:
-#   filename:  extra_fields_ignore.json
+#   filename:  extra_fields.json
 #   timestamp: 2019-07-26T00:00:00+00:00
 
 from __future__ import annotations
diff --git a/tests/data/expected/main/jsonschema/extra_fields_v2_allow.py b/tests/data/expected/main/jsonschema/extra_fields_v2_allow.py
new file mode 100644
index 000000000..c7ac5655f
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/extra_fields_v2_allow.py
@@ -0,0 +1,39 @@
+# generated by datamodel-codegen:
+#   filename:  extra_fields.json
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from typing import Optional
+
+from pydantic import BaseModel, ConfigDict
+
+
+class Foo(BaseModel):
+    model_config = ConfigDict(
+        extra='allow',
+    )
+    x: Optional[int] = None
+
+
+class Bar(BaseModel):
+    model_config = ConfigDict(
+        extra='allow',
+    )
+    y: Optional[int] = None
+
+
+class Baz(BaseModel):
+    model_config = ConfigDict(
+        extra='allow',
+    )
+    z: Optional[int] = None
+
+
+class Test(BaseModel):
+    model_config = ConfigDict(
+        extra='allow',
+    )
+    foo: Foo
+    bar: Optional[Bar] = None
+    baz: Optional[Baz] = None
diff --git a/tests/data/expected/main/jsonschema/extra_fields_v2_forbid.py b/tests/data/expected/main/jsonschema/extra_fields_v2_forbid.py
new file mode 100644
index 000000000..6c0018488
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/extra_fields_v2_forbid.py
@@ -0,0 +1,39 @@
+# generated by datamodel-codegen:
+#   filename:  extra_fields.json
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from typing import Optional
+
+from pydantic import BaseModel, ConfigDict
+
+
+class Foo(BaseModel):
+    model_config = ConfigDict(
+        extra='forbid',
+    )
+    x: Optional[int] = None
+
+
+class Bar(BaseModel):
+    model_config = ConfigDict(
+        extra='forbid',
+    )
+    y: Optional[int] = None
+
+
+class Baz(BaseModel):
+    model_config = ConfigDict(
+        extra='forbid',
+    )
+    z: Optional[int] = None
+
+
+class Test(BaseModel):
+    model_config = ConfigDict(
+        extra='forbid',
+    )
+    foo: Foo
+    bar: Optional[Bar] = None
+    baz: Optional[Baz] = None
diff --git a/tests/data/expected/main/jsonschema/extra_fields_v2_ignore.py b/tests/data/expected/main/jsonschema/extra_fields_v2_ignore.py
new file mode 100644
index 000000000..6c1b97eb1
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/extra_fields_v2_ignore.py
@@ -0,0 +1,39 @@
+# generated by datamodel-codegen:
+#   filename:  extra_fields.json
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from typing import Optional
+
+from pydantic import BaseModel, ConfigDict
+
+
+class Foo(BaseModel):
+    model_config = ConfigDict(
+        extra='ignore',
+    )
+    x: Optional[int] = None
+
+
+class Bar(BaseModel):
+    model_config = ConfigDict(
+        extra='ignore',
+    )
+    y: Optional[int] = None
+
+
+class Baz(BaseModel):
+    model_config = ConfigDict(
+        extra='ignore',
+    )
+    z: Optional[int] = None
+
+
+class Test(BaseModel):
+    model_config = ConfigDict(
+        extra='ignore',
+    )
+    foo: Foo
+    bar: Optional[Bar] = None
+    baz: Optional[Baz] = None
diff --git a/tests/data/jsonschema/extra_fields_ignore.json b/tests/data/jsonschema/extra_fields.json
similarity index 100%
rename from tests/data/jsonschema/extra_fields_ignore.json
rename to tests/data/jsonschema/extra_fields.json
diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py
index 4f7ea5e70..ca838b286 100644
--- a/tests/main/jsonschema/test_main_jsonschema.py
+++ b/tests/main/jsonschema/test_main_jsonschema.py
@@ -3249,19 +3249,56 @@ def test_main_json_pointer_percent_encoded_segments() -> None:
         assert "".join(result.split()) == "".join(expected.split())
 
 
+@pytest.mark.parametrize(
+    ("extra_fields", "output_model", "expected_output"),
+    [
+        (
+            "allow",
+            "pydantic.BaseModel",
+            "extra_fields_allow.py",
+        ),
+        (
+            "forbid",
+            "pydantic.BaseModel",
+            "extra_fields_forbid.py",
+        ),
+        (
+            "ignore",
+            "pydantic.BaseModel",
+            "extra_fields_ignore.py",
+        ),
+        (
+            "allow",
+            "pydantic_v2.BaseModel",
+            "extra_fields_v2_allow.py",
+        ),
+        (
+            "forbid",
+            "pydantic_v2.BaseModel",
+            "extra_fields_v2_forbid.py",
+        ),
+        (
+            "ignore",
+            "pydantic_v2.BaseModel",
+            "extra_fields_v2_ignore.py",
+        ),
+    ],
+)
 @freeze_time("2019-07-26")
-def test_main_extra_fields_ignore() -> None:
+def test_main_extra_fields(extra_fields: str, output_model: str, expected_output: str) -> None:
     with TemporaryDirectory() as output_dir:
         output_file: Path = Path(output_dir) / "output.py"
         return_code: Exit = main([
             "--input",
-            str(JSON_SCHEMA_DATA_PATH / "extra_fields_ignore.json"),
+            str(JSON_SCHEMA_DATA_PATH / "extra_fields.json"),
             "--output",
             str(output_file),
             "--input-file-type",
             "jsonschema",
             "--extra-fields",
-            "ignore",
+            extra_fields,
+            "--output-model-type",
+            output_model,
         ])
         assert return_code == Exit.OK
-        assert output_file.read_text() == (EXPECTED_JSON_SCHEMA_PATH / "extra_fields_ignore.py").read_text()
+        assert output_file.read_text() == (EXPECTED_JSON_SCHEMA_PATH / expected_output).read_text()