Skip to content

enable MISSING and NoDefault sentinel#1656

Open
Ksauder wants to merge 17 commits intovitalik:masterfrom
Ksauder:enable-missing-sentinel
Open

enable MISSING and NoDefault sentinel#1656
Ksauder wants to merge 17 commits intovitalik:masterfrom
Ksauder:enable-missing-sentinel

Conversation

@Ksauder
Copy link
Copy Markdown
Contributor

@Ksauder Ksauder commented Jan 22, 2026

#1655

I've been struggling with model schemas having null as an option for fields that I simply want to omit from the response. If the keys don't exist in the response, then there is no data. The current implementation does not allow changing how create_schema handles nullable fields and it always assigns a type | None = None. This causes all of my generated api clients to type hint incorrectly which is really annoying. exclude_unset also does not work for modelschemas since it's technically set since the model instance is interrogated via attributes and returns an explicit None for nullable fields and it does not help generate an accurate json schema.

The main feature of this PR is allowing modification to the type and value used when a field is "nullable". This allows MISSING to be used, which will create the data schema I'm looking for, while also supporting other type/value combos.

from typing import Literal

from django.db import models
from ninja.orm import create_schema
from ninja import NinjaAPI
from pydantic.experimental.missing_sentinel import MISSING

from devtools import debug


class Profile(models.Model):
    name = models.CharField(max_length=10)
    nullable = models.CharField(max_length=10, blank=True, null=True)

    class Meta:
        app_label = "tests"


ProfileModelSchema1 = create_schema(
    Profile,
    name="ProfileModelSchema1",
    fields=["name", "nullable"],
    nullable_type=MISSING,
    nullable_value=MISSING,
)


ProfileModelSchema2 = create_schema(
    Profile,
    name="ProfileModelSchema2",
    fields=["name", "nullable"],
    nullable_type=Literal["MyNullValue"],
    nullable_value="MyNullValue",
)

api = NinjaAPI()


@api.get("/profile1", response=ProfileModelSchema1)
def get_profile1(request):
    return Profile(name="Name")


@api.get("/profile2", response=ProfileModelSchema2)
def get_profile2(request):
    return Profile(name="Name")

api.get_openapi_json() == {
        ...
        'components': {
            'schemas': {
                'ProfileModelSchema1': {
                    'properties': {
                        'name': {
                            'maxLength': 10,
                            'title': 'Name',
                            'type': 'string',
                        },
                        'nullable': {
                            'maxLength': 10,
                            'title': 'Nullable',
                            'type': 'string',
                        },
                    },
                    'required': ['name'],
                    'title': 'ProfileModelSchema1',
                    'type': 'object',
                },
                'ProfileModelSchema2': {
                    'properties': {
                        'name': {
                            'maxLength': 10,
                            'title': 'Name',
                            'type': 'string',
                        },
                        'nullable': {
                            'anyOf': [
                                {
                                    'maxLength': 10,
                                    'type': 'string',
                                },
                                {
                                    'const': 'MyNullValue',
                                    'type': 'string',
                                },
                            ],
                            'default': 'MyNullValue',
                            'title': 'Nullable',
                        },
                    },
                    'required': ['name'],
                    'title': 'ProfileModelSchema2',
                    'type': 'object',
                },
            },
        },
        ...
    }

@vitalik
Copy link
Copy Markdown
Owner

vitalik commented Jan 23, 2026

hi @Ksauder - would you be able to add some matix to test with pydantic < 2.11 - would not run against all python versions but just one latest

@Ksauder
Copy link
Copy Markdown
Contributor Author

Ksauder commented Jan 23, 2026

Sure I can look into that. I'm still working out actually using this in practice. I'm debugging some issues when a schema with MISSING is actually used as a return schema from an operation.

@Ksauder Ksauder marked this pull request as draft January 23, 2026 21:18
@Ksauder Ksauder force-pushed the enable-missing-sentinel branch 3 times, most recently from 7e41aea to af717db Compare February 9, 2026 05:11
@Ksauder Ksauder marked this pull request as ready for review February 9, 2026 05:11
@Ksauder
Copy link
Copy Markdown
Contributor Author

Ksauder commented Feb 9, 2026

@vitalik Okay, I think this is a pretty clean implementation. Added two args to create_schema and relevant child functions nullable_value and nullable_type with default from conf, NULLABLE_FIELD_*. Had to do this to get MISSING down to the model schema level.
And added MISSING to the check in default_schema so we don't try to encode it.

@Ksauder
Copy link
Copy Markdown
Contributor Author

Ksauder commented Feb 12, 2026

hi @Ksauder - would you be able to add some matix to test with pydantic < 2.11 - would not run against all python versions but just one latest

@vitalik Actually, I'm not sure how you would like this implemented. I'm not familiar with the github CICD, and I assume you want it there. How exactly would you like this done?

@Ksauder Ksauder force-pushed the enable-missing-sentinel branch 3 times, most recently from 1cbc36f to bc73f2c Compare April 11, 2026 02:15
@Ksauder Ksauder force-pushed the enable-missing-sentinel branch from bc73f2c to f0b3fe3 Compare April 11, 2026 02:21
@Ksauder Ksauder force-pushed the enable-missing-sentinel branch from 310ca57 to 35f3522 Compare April 11, 2026 02:48
@Ksauder
Copy link
Copy Markdown
Contributor Author

Ksauder commented Apr 11, 2026

@vitalik Hi, I just rebased, cleaned up, and got the test matrix done. Any interest in this or anything I can do to move it forward?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants