From 507dfcd39155eb0e75acd885cba40063cc2b6636 Mon Sep 17 00:00:00 2001 From: "daichi.tomaru" Date: Sun, 3 Aug 2025 17:42:19 +0900 Subject: [PATCH] add strict_null option for simply converting field's nullable from model to schema --- ninja/orm/factory.py | 2 + ninja/orm/fields.py | 12 ++++- ninja/orm/metaclass.py | 5 ++ tests/test_orm_metaclass.py | 104 +++++++++++++++++++++++++++++++++++- 4 files changed, 120 insertions(+), 3 deletions(-) diff --git a/ninja/orm/factory.py b/ninja/orm/factory.py index 3416df6ec..e36040f6d 100644 --- a/ninja/orm/factory.py +++ b/ninja/orm/factory.py @@ -41,6 +41,7 @@ def create_schema( fields: Optional[List[str]] = None, exclude: Optional[List[str]] = None, optional_fields: Optional[List[str]] = None, + fields_strict_null: bool = False, custom_fields: Optional[List[Tuple[str, Any, Any]]] = None, base_class: Type[Schema] = Schema, ) -> Type[Schema]: @@ -66,6 +67,7 @@ def create_schema( fld, depth=depth, optional=optional_fields and (fld.name in optional_fields), + strict_null=fields_strict_null, ) definitions[fld.name] = (python_type, field_info) diff --git a/ninja/orm/fields.py b/ninja/orm/fields.py index d67814c8c..416c4628a 100644 --- a/ninja/orm/fields.py +++ b/ninja/orm/fields.py @@ -115,7 +115,11 @@ def _validate(cls, v: Any, _): @no_type_check def get_schema_field( - field: DjangoField, *, depth: int = 0, optional: bool = False + field: DjangoField, + *, + depth: int = 0, + optional: bool = False, + strict_null: bool = False, ) -> Tuple: "Returns pydantic field from django's model field" alias = None @@ -163,7 +167,11 @@ def get_schema_field( ] raise ConfigError("\n".join(msg)) from e - if field.primary_key or blank or null or optional: + if strict_null: + if optional or null: + default = None + nullable = True + elif field.primary_key or blank or null or optional: default = None nullable = True diff --git a/ninja/orm/metaclass.py b/ninja/orm/metaclass.py index 84605aa69..8f1136d8c 100644 --- a/ninja/orm/metaclass.py +++ b/ninja/orm/metaclass.py @@ -17,6 +17,7 @@ class MetaConf: fields: Optional[List[str]] = None exclude: Union[List[str], str, None] = None fields_optional: Union[List[str], str, None] = None + fields_strict_null: bool = False @staticmethod def from_schema_class(name: str, namespace: dict) -> "MetaConf": @@ -26,6 +27,7 @@ def from_schema_class(name: str, namespace: dict) -> "MetaConf": fields = getattr(meta, "fields", None) exclude = getattr(meta, "exclude", None) optional_fields = getattr(meta, "fields_optional", None) + fields_strict_null = getattr(meta, "fields_strict_null", False) elif "Config" in namespace: config = namespace["Config"] @@ -33,6 +35,7 @@ def from_schema_class(name: str, namespace: dict) -> "MetaConf": fields = getattr(config, "model_fields", None) exclude = getattr(config, "model_exclude", None) optional_fields = getattr(config, "model_fields_optional", None) + fields_strict_null = getattr(config, "fields_strict_null", False) warnings.warn( "The use of `Config` class is deprecated for ModelSchema, use 'Meta' instead", @@ -62,6 +65,7 @@ def from_schema_class(name: str, namespace: dict) -> "MetaConf": fields=fields, exclude=exclude, fields_optional=optional_fields, + fields_strict_null=fields_strict_null, ) @@ -109,6 +113,7 @@ def __new__( fields=meta_conf.fields, exclude=meta_conf.exclude, optional_fields=meta_conf.fields_optional, + fields_strict_null=meta_conf.fields_strict_null, custom_fields=custom_fields, base_class=cls, ) diff --git a/tests/test_orm_metaclass.py b/tests/test_orm_metaclass.py index 58838a15e..bd0ec2c40 100644 --- a/tests/test_orm_metaclass.py +++ b/tests/test_orm_metaclass.py @@ -1,7 +1,9 @@ +from typing import Optional + import pytest from django.db import models -from ninja import ModelSchema +from ninja import Field, ModelSchema from ninja.errors import ConfigError @@ -183,3 +185,103 @@ def test_model_schema_without_config(): class NoConfigSchema(ModelSchema): x: int + + +def test_field_strict_null(): + class ModelForTestFieldStrictNull(models.Model): + blank_false_null_false = models.CharField(blank=False, null=False) + blank_false_null_true = models.CharField(blank=False, null=True) + blank_true_null_false = models.CharField(blank=True, null=False) + blank_true_null_true = models.CharField(blank=True, null=True) + + class Meta: + app_label = "tests" + + class FieldStrictNullFalseSchema(ModelSchema): + class Meta: + model = ModelForTestFieldStrictNull + fields = "__all__" + + class FieldStrictNullTrueSchema(ModelSchema): + class Meta: + model = ModelForTestFieldStrictNull + fields = "__all__" + fields_strict_null = True + + class FieldStrictNullTrueWithOptionalFieldsSchema(ModelSchema): + blank_true_null_false: Optional[str] = Field(None) + + class Meta: + model = ModelForTestFieldStrictNull + fields = [ + "id", + "blank_false_null_false", + ] + fields_strict_null = True + fields_optional = ["id", "blank_false_null_false"] + + assert FieldStrictNullFalseSchema.json_schema() == { + "title": "FieldStrictNullFalseSchema", + "type": "object", + "properties": { + "id": {"title": "ID", "anyOf": [{"type": "integer"}, {"type": "null"}]}, + "blank_false_null_false": { + "title": "Blank False Null False", + "type": "string", + }, + "blank_false_null_true": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Blank False Null True", + }, + "blank_true_null_false": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Blank True Null False", + }, + "blank_true_null_true": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Blank True Null True", + }, + }, + "required": ["blank_false_null_false"], + } + + assert FieldStrictNullTrueSchema.json_schema() == { + "title": "FieldStrictNullTrueSchema", + "type": "object", + "properties": { + "id": {"title": "ID", "type": "integer"}, + "blank_false_null_false": { + "title": "Blank False Null False", + "type": "string", + }, + "blank_false_null_true": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Blank False Null True", + }, + "blank_true_null_false": { + "title": "Blank True Null False", + "type": "string", + }, + "blank_true_null_true": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Blank True Null True", + }, + }, + "required": ["id", "blank_false_null_false", "blank_true_null_false"], + } + + assert FieldStrictNullTrueWithOptionalFieldsSchema.json_schema() == { + "title": "FieldStrictNullTrueWithOptionalFieldsSchema", + "type": "object", + "properties": { + "id": {"title": "ID", "anyOf": [{"type": "integer"}, {"type": "null"}]}, + "blank_false_null_false": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Blank False Null False", + }, + "blank_true_null_false": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Blank True Null False", + }, + }, + }