django-ninja
django-ninja copied to clipboard
[BUG] Unexpected Behavior of ModelSchema for fields with default
Description
Hey, it's me again.
Creating schema instances from an empty dictionary generates values for fields that have a default defined.
Maybe i misunderstand how ModelSchemas are supposed to work, but I perceive this as unexpected behavior.
Expected behavior would be that ValidationError is raised if I don't provide valid values for non-optional fields, but apparently some values magically appear.
See example code below.
I stumbled over this, because I applied ModelSchemas for incoming requests and I wondered why some requests went through which should have failed. After again reading the documentation very carefully - it seems to me that ModelSchema is not recommended for request validation. If that's the case I think it should be made much clearer, maybe even disallow request validation with ModelSchema?
Model
import random
import string
import uuid
from django.db import models
def generate_rnd_string():
return ''.join(random.choice(string.ascii_lowercase) for i in range(8))
class MyModel(models.Model):
some_datetime = models.DateTimeField(null=True)
just_a_uuid = models.UUIDField(default=uuid.uuid4, editable=False)
random_string = models.CharField(
default=generate_rnd_string,
max_length=8,
null=False
)
string_without_default = models.CharField(
null=True,
max_length=8,
)
Schema
from pydantic.types import UUID
from myapp.models import MyModel
class MyModelSchema(ModelSchema):
class Config:
model = MyModel
model_fields = [
"some_datetime",
"just_a_uuid",
"random_string",
"string_without_default",
]
Test code
from django.test import TestCase
from myapp.schemas import MySchema, MyModelSchema
class ModelSchemaBehaviorTest(TestCase):
def test_modelschema(self):
modelschema_instance = MyModelSchema(**{})
print(modelschema_instance.__dict__)
Result
{'just_a_uuid': UUID('42f3fe65-74f2-4d24-b83f-8841a58573c9'),
'random_string': 'gcgnmjnc',
'some_datetime': None,
'string_without_default': None}
I don't see why this is unexpected behaviour. Isn't this exactly what you'd expect when you specify a default (defined as "a selection automatically used by a program in the absence of a choice made by the user")?
What would be the purpose of default if it didn't do this?
You're right, that's what i want from a default - when I want to create an instance of that particular model. And that's something Django is doing just fine imo.
What are the use cases for having the Schema provide a default value? I guess one is to generate an example for OpenAPI.
Here is my issue (and as I said, I might have misunderstood how ModelSchema is supposed to be used):
Imagine having a field secret_code which is generated with the default function when I instantiate the corresponding model. I now have an API endpoint that requires a user to provide me with that value for an existing model instance so I can verify his request.
If I use a ModelSchema to validate the request, and the user doesn't provide secret_code, I want to notice that in the endpoint when the json is validated.
If the schema generates a default value, the validation will throw no exception. Furthermore, i will hit the database trying to verify the secret_code generated by the schema.
well defaults seems practical on my experience (when need to submit large objects using swagger UI)
but yeah... the callable might not make sense (like functions that return timestamps, secrets, etc)
maybe workaround would be to ignore callable defaults from models (but set required to False instead)
I was also thinking about editable=False setting these generally does not make sense for POST/PUT requests, but can be used for GET...
I think this would be solved by setting some model fields explicitly required. Something like this:
class MyModelSchemaCreate(NinjaSchema):
class Config:
model = MyModel
model_fields = ["some_uuid", "secret"]
class MyModelSchema(NinjaSchema):
class Config:
model = MyModel
model_fields = ["some_uuid", "secret"]
required_fields = ["secret"]
modelschema_instance = MyModelSchemaCreate(**{}) # {"some_uuid": "0b5fb3c8-f27d-4996-bf7f-c40f26a3253d","secret": "This is secret"}
modelschema_instance = MyModelSchema(**{}) # Throws an error because 'secret' was not supplied
The second behaviou should correspond to this:
class MyModelSchema(Schema):
some_uuid: UUID | None # Optional because the model supplies a default value
secret: str # Not optional becuase it was declared as required explicitly
This would allow to explicitly require certain fields in the request, even though they have a default database value. You could then use the first schema for object creation, when you don't mind not getting the value in the request (database default will supply that). But you could use the second schema to explicitly require receiving the value in the request, which would solve @bertraol scenario:
Imagine having a field
secret_codewhich is generated with the default function when I instantiate the corresponding model. I now have an API endpoint that requires a user to provide me with that value for an existing model instance so I can verify his request.If I use a ModelSchema to validate the request, and the user doesn't provide
secret_code, I want to notice that in the endpoint when the json is validated.If the schema generates a default value, the validation will throw no exception. Furthermore, i will hit the database trying to verify the
secret_codegenerated by the schema.
I think that there is still an issue with default argument of model field.
If I use ModelSchema class to define schema for instance creation endpoint like (POST, PUT, PATCH) then fields having default value/callable in a model definition will be marked as not required in documentation which is ok.
But if I want to create schema for representing a data from database (GET) then it will be marked as non-required (possible missing) but it should be marked as required.
So I think it's better to ignore default and rely just on null, or probably to make it configurable.