Skip to content

🚀 Feature Request: Support NotRequired Field Semantics in Pydantic Models #11705

@Haskely

Description

@Haskely

Initial Checks

  • I have searched Google & GitHub for similar requests and couldn't find anything
  • I have read and followed the docs and still think this feature is missing

Description

🚀 Feature Request: Support NotRequired Field Semantics in Pydantic Models

Background

In many real-world API contracts, some request fields are:

  • Optional to include (i.e., the client may omit them entirely),
  • But if provided, must be a valid non-null value,
  • And null is not a valid input (and often not semantically meaningful),
  • Meanwhile, default values might exist on either client or server side,
  • And sometimes you want to keep Pydantic’s default values for internal logic, while only omitting them from serialized outputs under certain rules.

Currently, Pydantic does not provide a clean, expressive way to represent this intent.


🔍 Motivation

Consider the following server-side API contract:

POST /predict
{
  "model": "gpt-4",
  "temperature": 0.7   // optional, but if provided must be a float between 0 and 1
}

The rules are:

  1. The temperature field is optional — if omitted, the server uses its own default.
  2. If provided, it must be a float, and must not be null.
  3. The client should be able to omit it without sending a dummy value like 0.0.

🧪 Existing Workarounds (and Their Issues)

1. temperature: float | None = None

class Request(BaseModel):
    model: str
    temperature: float | None = None

Problem:

  • This accepts "temperature": null, which violates the API contract.
  • null is not a valid temperature — but this model would allow it.

2. Use .model_dump(exclude_none=True)

Problem:

  • Some fields may legitimately use None as a value (e.g. description: str | None).
  • exclude_none=True will strip all Nones, even if some are semantically meaningful.
  • No per-field control over None exclusion.

3. Use temperature: float = 0.0 and .model_dump(exclude_defaults=True)

class Request(BaseModel):
    model: str
    temperature: float = 0.0

Problem:

  • 0.0 might be a valid and meaningful temperature.
  • Can't distinguish between "use server default" vs. "I want zero".
  • exclude_defaults=True affects all fields, not just temperature.

4. Use .model_dump(exclude_unset=True)

class Request(BaseModel):
    model: str
    temperature: float = 0.7

Problem:

  • Works for temperature, but fails for cases where you want the defaulted field to be included.

Example:

class FunctionTool(BaseModel):
    type: Literal["function"] = "function"
    function: dict
  • type should always be included in the output (required by server).
  • But exclude_unset=True will remove it unless explicitly set by the user.
  • Again, too broad — no per-field control.

5. Use TypedDict + TypeAdapter (which supports NotRequired)

from typing import TypedDict, NotRequired
from pydantic import TypeAdapter

class RequestDict(TypedDict, total=False):
    model: str
    temperature: NotRequired[float]

TypeAdapter(RequestDict).validate_python({"model": "gpt-4"})

Problem:

  • TypedDict does not support default values.

  • For example:

    class FunctionToolDict(TypedDict):
        type: Literal["function"]  # must be manually set every time
        function: dict
    • There's no way to pre-fill type = "function" as a default.
    • Developer must remember to manually set it every time — error-prone and inconvenient.
  • TypedDict also lacks many useful features from BaseModel:

    • Validation logic,
    • Serialization methods (model_dump),
    • Field validators, default factories, etc.

In short: TypedDict gives you NotRequired, but takes away everything else that makes Pydantic useful.


🧩 What's Missing: NotRequired

We need a way to express this common API pattern:

“This field is optional to include, but if it is included, it must be non-null and type-valid.”

Something like:

from pydantic import BaseModel, NotRequired

class Request(BaseModel):
    model: str
    temperature: NotRequired[float]

This would:

  • Validate properly (disallow None),
  • Exclude the field from .model_dump() if unset,
  • Allow defaulted fields like type = "function" to still work,
  • Avoid global exclusions like exclude_none or exclude_defaults,
  • Make intent explicit and models more maintainable.

✅ Benefits

  • Expressivity: Cleanly encodes a common contract pattern.
  • Precision: Avoids accepting None when it isn't semantically valid.
  • Field-level control: Unlike global flags like exclude_none.
  • Developer ergonomics: No need for boilerplate logic or manual defaults.
  • Type-safe and declarative: Self-documenting and easy to understand.

🧪 Realistic Example Use Case

class JobConfig(BaseModel):
    id: str
    retry: NotRequired[int]              # optional override; must be non-null if set
    timeout: int = 60                    # always included
    description: str | None = None       # null is meaningful here
  • retry: Optional override — server has a default, but client can set.
  • timeout: Required by server; included by default.
  • description: Can be null — unlike retry.

This pattern is very hard to model cleanly with current Pydantic tools.


🙏 Feature Request

Would the Pydantic team consider supporting a NotRequired[T] type hint?

It would fill the semantic gap between Optional[T], defaulted fields, and serialization control — and would greatly simplify many real-world use cases involving API request modeling.

Thanks for the amazing work you do 💙


Related Issue

#9057
#8394
#7712

Affected Components

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions