From d39eb705dfc12fa3ce4259a3cd702e8f95d73052 Mon Sep 17 00:00:00 2001 From: Peter Doggart Date: Mon, 22 Sep 2025 20:08:57 +0000 Subject: [PATCH] A fix to allow nullable fields.nested for input validation. --- CHANGELOG.rst | 2 +- flask_restx/fields.py | 18 ++++++++++++++++++ tests/test_fields.py | 11 +++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a63e2405..bc7f48fe 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,7 +35,7 @@ Bug Fixes :: * Add python version requirement on setup.py (#586) [jason-the-j] - + * Fix Nested field schema generation for nullable fields. (#638) [peter-doggart] .. _section-1.3.0: 1.3.0 diff --git a/flask_restx/fields.py b/flask_restx/fields.py index 328b437c..4484ef51 100644 --- a/flask_restx/fields.py +++ b/flask_restx/fields.py @@ -134,6 +134,9 @@ class Raw(object): :param bool readonly: Is the field read only ? (for documentation purpose) :param example: An optional data example (for documentation purpose) :param callable mask: An optional mask function to be applied to output + :param bool nullable: Whether the field accepts null values in input + validation. When True, the generated JSON Schema will allow null + values for this field during request payload validation. """ #: The JSON/Swagger schema type @@ -153,6 +156,7 @@ def __init__( readonly=None, example=None, mask=None, + nullable=None, **kwargs ): self.attribute = attribute @@ -163,6 +167,7 @@ def __init__( self.readonly = readonly self.example = example if example is not None else self.__schema_example__ self.mask = mask + self.nullable = nullable def format(self, value): """ @@ -284,6 +289,19 @@ def schema(self): schema["allOf"] = allOf else: schema["$ref"] = ref + + # If nullable is True, wrap using anyOf to permit nulls for input validation + if self.nullable: + # Remove structural keys that conflict with anyOf composition + for key in ("$ref", "allOf", "type", "items"): + schema.pop(key, None) + # Create anyOf with the original schema and null type + anyOf = [{"$ref": ref}] + if self.as_list: + anyOf = [{"type": "array", "items": {"$ref": ref}}] + anyOf.append({"type": "null"}) + schema["anyOf"] = anyOf + return schema def clone(self, mask=None): diff --git a/tests/test_fields.py b/tests/test_fields.py index 7b0333a4..9f255c5c 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -881,6 +881,17 @@ def test_with_allow_null(self, api): assert field.allow_null assert field.__schema__ == {"$ref": "#/definitions/NestedModel"} + def test_with_nullable_schema(self, api): + nested_fields = api.model("NestedModel", {"name": fields.String}) + field = fields.Nested(nested_fields, nullable=True) + # Should allow null in schema via anyOf + assert field.__schema__ == { + "anyOf": [ + {"$ref": "#/definitions/NestedModel"}, + {"type": "null"}, + ] + } + def test_with_skip_none(self, api): nested_fields = api.model("NestedModel", {"name": fields.String}) field = fields.Nested(nested_fields, skip_none=True)