Pydantic API models: preserve partial updates and reject invalid input
A Pydantic skill can help an agent draft API models, but a working class does not settle the meaning of a partial update. This guide uses a pinned Microsoft template and a small local experiment to show how to preserve omitted fields, deliberately clear a value, and keep server-owned fields outside a request contract.
Written by BB Skills with AI assistance for drafting and source review. The independently authored fixture, recorded observations and unchanged Microsoft files are available in Pydantic API Model Contracts. The examples use synthetic projects and make no network requests.
What should a Pydantic skill help you decide?
Start with the API behavior you need. Creation, partial updates, public responses and database documents have different responsibilities. Give the agent a field list, the fields clients may edit, a rule for explicit null values, and examples of expected errors. Ask for models and focused tests before connecting them to a router or database.
For a partial update, collect only fields the caller supplied with model_dump(exclude_unset=True). Decide which supplied nulls are valid. Merge permitted changes into the existing state, then validate the resulting object. Check ownership and authorization in the application before saving any change. A model does not establish who may update a record.
Separate model responsibilities before adding fields
The Microsoft resource provides a compact template rather than a complete API service. Its main file links to an actual Python template and a capability reference. Keep those files together when installing it. The following decisions are BB Skills guidance for adapting that template to a project.
| Contract | Question to answer | Example boundary |
|---|---|---|
| Create | What must a client supply to create an object? | Require a name and the permitted workspace identifier; derive the author from the authenticated request. |
| Update | Which fields may change, and what does omission mean? | Allow name and description changes; preserve a description when it is absent. |
| Response | What may leave the service? | Expose declared public fields; do not return a raw database object. |
| InDB | What extra fields does storage need? | Keep a document discriminator separate from the client request. This class does not perform persistence. |
Shared fields can reduce duplication, but inheritance should not accidentally make internal fields client-writable. Add the client contract first, then a separate response contract. Use a database model only when its extra document fields serve your actual storage design.
Treat an omitted value differently from explicit null
In our fixture, the original update model accepts optional name and description fields. Constructing an update containing only a new name and dumping it normally produces both the new name and a null description. If an application blindly writes that dictionary, it can erase text the client did not change. This is an integration mistake to prevent, not evidence that a particular deployed API is vulnerable.
patch = ProjectUpdate.model_validate(payload).model_dump(exclude_unset=True)
merged = {**existing, **patch}
validated = ProjectCreate.model_validate(merged)
Here existing must contain the fields needed by the creation contract. The project example keeps workspaceId from the existing state. Do not copy untrusted request data into that state before validation.
| Incoming update | Explicit patch | Fixture behavior |
|---|---|---|
{} | {} | No fields change. Your API must decide whether an empty patch is an accepted no-op. |
{"name": "Renamed"} | Name only | The previous description stays intact. |
{"description": null} | Description is null | The fixture deliberately clears the optional description. |
{"name": null} | Name is null | The merged creation contract rejects a null required name. |
exclude_none=True removes null values even when supplied explicitly. That is a different policy and would remove the description-clearing instruction in this example. Choose the policy from the product contract rather than using either option as a universal fix. See Pydantic's serialization reference.
Reject unexpected input where the request contract requires it
The pinned template uses the default behavior for extra input: our update experiment supplied an authorId and observed that it was ignored. An ignored field is not an error response. If a client must receive an error for unexpected input, configure that behavior explicitly.
class PatchProject(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str | None = Field(default=None, min_length=1, max_length=200)
description: str | None = Field(default=None, max_length=2000)
This BB Skills example imports BaseModel, ConfigDict and Field from Pydantic; the complete file is in the package. It rejects a supplied workspace change, then revalidates the merged object through the creation model. The fixture also demonstrates a stricter creation variant that rejects an injected author field.
Rejection of unknown input does not make an allowed field authorized. The route still needs ownership checks, permitted workspace access, concurrency handling and a deliberate persistence operation. For review of those surrounding decisions, use Evidence-Based Security Code Review with the actual route and its tests.
Check aliases and the actual response output
The original template accepts a camelCase workspace alias and its Python field name. Our fixture checks both paths and explicitly serializes a response using by_alias=True. It also uses JSON serialization for a date rather than passing a Python datetime directly to a JSON encoder.
The template uses populate_by_name=True. Current Pydantic configuration documentation recommends validate_by_name=True together with validate_by_alias=True for v2.11 and later. Keep the unchanged upstream file for provenance; adapt a separate project copy to the Pydantic version you pin and test. Do not assume accepting an alias means responses automatically use it.
One response observation creates a synthetic object with an undeclared password field and verifies that projection through the declared response model omits it. That result applies to this model and serialization path. Review custom serializers, subclass behavior and fields declared in your real response before treating projection as a privacy boundary.
What the recorded experiment proves
BB Skills replaced only the documented resource-name placeholders in the pinned template and ran the same 19 named observations on Python 3.12.4 with Pydantic 2.13.5 and 2.7.4. Both runs passed. The recorded files identify the Windows environment, UTC execution time and SHA-256 hashes of the template and fixture code. The current-version run used its own disposable virtual environment; the older run is retained as a comparison, not a recommendation to downgrade.
The observations cover creation aliases, required workspace input, length limits, omitted and null update fields, extra-input behavior, explicit rejection in the independent contract, merged-state validation, date serialization, response projection, the document discriminator and generated schema. Expected validation errors count as a passed observation only when the expected field and error type match.
This is direct execution of a template and an independent example. No AI client loaded and executed the complete skill. No production route, database write, live account, payment flow, cloud SDK or Microsoft evaluation harness was exercised. The catalog therefore keeps runtime_tested=false. Nineteen observations do not certify all Pydantic versions or all API security properties.
Install the whole package and reproduce locally
- Open the resource profile, inspect the license, source commit, package file list and current checksum.
- Download the free ZIP and keep its
pydantic-models-pyfolder intact. Place it in the skills directory documented by your chosen agent; the original template and reference use relative paths. - Create a disposable Python virtual environment. Install the Pydantic version recorded in the evidence you want to reproduce, or your application's pinned v2 version. The package does not bundle Python or dependency wheels.
- From the package root, run
python examples/run_validation_fixture.pywith assertions enabled. Review the script first. It reads the pinned template, uses synthetic inputs and writes a local JSON report; it makes no network requests. - Compare the report's named cases and environment with the packaged evidence. Rerun adapted models in your own test environment before integrating them with application routes.
Use Modern Python Project Tooling for environment and project setup, then Test Driven Development to turn your real API contract into tests. These are complementary workflows; installing several skills does not verify compatibility among them. Dependency downloads and this fixture are free; no API key or paid model call is required.
Sources and next steps
The upstream package is Microsoft-authored, MIT-licensed source pinned to commit 354361d83247c76a1c21e802e0d4887c4d8323a3. The original skill directory, full license, actual asset and capability reference are preserved. The unchanged upstream acceptance criteria are included as reference material; their hosted harness was not run.
For framework behavior, consult Pydantic model documentation, the serialization and configuration references linked above, and the version-specific release notes when changing dependencies. For your next review, bring a creation request, an omitted-field patch, an explicit-null patch, an unauthorized-field attempt and the expected public response. That makes the skill's output assessable against the behavior your API needs.