Skip to content

Commit 18edb0c

Browse files
committed
Preserve Field constraints in PatchDict
1 parent 31c1275 commit 18edb0c

File tree

2 files changed

+43
-10
lines changed

2 files changed

+43
-10
lines changed

ninja/patch_dict.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
from typing import (
23
TYPE_CHECKING,
34
Any,
@@ -9,13 +10,20 @@
910
)
1011

1112
from pydantic import BaseModel
13+
from pydantic.fields import FieldInfo
1214
from pydantic_core import core_schema
1315

1416
from ninja import Body
1517
from ninja.orm import ModelSchema
1618
from ninja.schema import Schema
1719
from ninja.utils import is_optional_type
1820

21+
try:
22+
copy_field_info = FieldInfo._copy
23+
except AttributeError:
24+
# Fallback for Pydantic<2.12.0
25+
copy_field_info = copy.copy
26+
1927

2028
class ModelToDict(dict):
2129
_wrapped_model: Any = None
@@ -45,15 +53,20 @@ def get_schema_annotations(schema_cls: Type[Any]) -> Dict[str, Any]:
4553
return annotations
4654

4755

48-
def create_patch_schema(schema_cls: Type[Any]) -> Type[ModelToDict]:
56+
def create_patch_schema(schema_cls: Type[BaseModel]) -> Type[ModelToDict]:
4957
schema_annotations = get_schema_annotations(schema_cls)
50-
values, annotations = {}, {}
51-
# assert False, f"{schema_cls} - {schema_cls.model_fields}"
52-
for f in schema_cls.model_fields.keys():
53-
t = schema_annotations[f]
54-
if not is_optional_type(t):
55-
values[f] = getattr(schema_cls, f, None)
56-
annotations[f] = Optional[t]
58+
values: Dict[str, Any] = {}
59+
annotations = {}
60+
61+
for name, field in schema_cls.model_fields.items():
62+
annotation = schema_annotations[name]
63+
if is_optional_type(annotation):
64+
continue
65+
patch_field = copy_field_info(field)
66+
patch_field.default = None
67+
patch_field.default_factory = None
68+
values[name] = patch_field
69+
annotations[name] = Optional[annotation]
5770
values["__annotations__"] = annotations
5871
OptionalSchema = type(f"{schema_cls.__name__}Patch", (schema_cls,), values)
5972

@@ -65,7 +78,7 @@ class OptionalDictSchema(ModelToDict):
6578

6679

6780
class PatchDictUtil:
68-
def __getitem__(self, schema_cls: Any) -> Any:
81+
def __getitem__(self, schema_cls: Type[BaseModel]) -> Any:
6982
new_cls = create_patch_schema(schema_cls)
7083
return Body[new_cls] # type: ignore
7184

tests/test_patch_dict.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from ninja import NinjaAPI, Schema
5+
from ninja import Field, NinjaAPI, Schema
66
from ninja.patch_dict import PatchDict
77
from ninja.testing import TestClient
88

@@ -15,6 +15,7 @@ class SomeSchema(Schema):
1515
name: str
1616
age: int
1717
category: Optional[str] = None
18+
identifier: str = Field(max_length=32)
1819

1920

2021
class OtherSchema(SomeSchema):
@@ -47,6 +48,11 @@ def test_patch_calls(input: dict, output: dict):
4748
assert response.json() == {"payload": output, "type": "<class 'dict'>"}
4849

4950

51+
def test_patch_calls_bad_request():
52+
response = client.patch("/patch", json={"identifier": "0" * 100})
53+
assert response.status_code == 422
54+
55+
5056
def test_schema():
5157
"Checking that json schema properties are all optional"
5258
schema = api.get_openapi_schema()
@@ -66,6 +72,13 @@ def test_schema():
6672
"anyOf": [{"type": "string"}, {"type": "null"}],
6773
"title": "Category",
6874
},
75+
"identifier": {
76+
"anyOf": [
77+
{"maxLength": 32, "type": "string"},
78+
{"type": "null"},
79+
],
80+
"title": "Identifier",
81+
},
6982
},
7083
}
7184

@@ -93,6 +106,13 @@ def test_inherited_schema():
93106
"anyOf": [{"type": "integer"}, {"type": "null"}],
94107
"title": "Age",
95108
},
109+
"identifier": {
110+
"anyOf": [
111+
{"maxLength": 32, "type": "string"},
112+
{"type": "null"},
113+
],
114+
"title": "Identifier",
115+
},
96116
"other": {
97117
"anyOf": [{"type": "string"}, {"type": "null"}],
98118
"title": "Other",

0 commit comments

Comments
 (0)