Todd Mercer

Pydantic Strict Mode: JSON and Python Inputs Are Not Equivalent
model_validate_json(payload, strict=True) and model_validate(json.loads(payload), strict=True) do not enforce identical input rules. In Pydantic strict mode, JSON representations of dates, UUIDs and tuples can be accepted where the equivalent decoded Python strings and lists are rejected. Strict JSON still rejects an integer field supplied as a numeric string or Boolean in the fixture below.
If you consume agent-generated job payloads, decide which boundary your application actually accepts. Then test that boundary before the validated values trigger downstream work. Switching to lax validation just to remove an error can admit coercions you did not intend.
The same payload, three different input policies
Consider a job containing a calendar date, a UUID, an integer retry count and a two-integer tuple. On the wire, JSON represents the first two as strings and the tuple as an array. After json.loads, those remain Python strings and a list; decoding has not constructed a date, UUID or tuple.
We tested that job model on October 2, 2026, using Python 3.13.14 and Pydantic 2.13.4 through Aident Loadout Code Interpreter. The same model, with extra="forbid", received 12 synthetic payloads under three policies:
Strict JSON:
Job.model_validate_json(wire, strict=True).Strict decoded Python:
Job.model_validate(json.loads(wire), strict=True).Lax decoded Python:
Job.model_validate(json.loads(wire), strict=False).
The first path accepts JSON representations appropriate to those field types. The second demands Python instances for the date, UUID and tuple in this model. The third can coerce additional values. These are type- and input-source-specific rules, not three quality scores.
What the 12-payload test accepted
Each row changes only the indicated field from the valid wire payload. The strict decoded-Python column still contains the base date/UUID strings and list, so every row fails that path. It does not prove that an otherwise correctly typed Python payload would fail.
Payload change | Strict JSON | Strict decoded Python | Lax decoded Python |
|---|---|---|---|
Valid wire representations | Accept | Reject | Accept |
Retry count | Reject | Reject | Accept |
Retry count | Reject | Reject | Accept |
Retry count | Reject | Reject | Accept |
Retry count | Reject | Reject | Reject |
Tuple elements | Reject | Reject | Accept |
Three elements for a two-element tuple | Reject | Reject | Reject |
Impossible date | Reject | Reject | Reject |
Midnight datetime string in the date field | Reject | Reject | Accept |
Invalid UUID string | Reject | Reject | Reject |
Unexpected field | Reject | Reject | Reject |
Null retry count | Reject | Reject | Reject |
The fixture made 36 acceptance assertions and four result assertions for a separately constructed typed Python payload. All 40 passed. That typed payload used an actual date, UUID and tuple and succeeded under strict Python validation.
The unexpected-field rejection comes from extra="forbid", not from strictness alone. Similarly, a valid date is not a scheduling authorization, and a valid UUID is not evidence that a job exists or belongs to the caller.
Choose a boundary instead of removing strictness
For a JSON-facing consumer, validate the raw JSON at the boundary if the model's JSON representation policy matches your contract. Assert both the returned types and the values you intend to use. A successful parse alone does not establish business validity.
For an internal Python interface, pass correctly typed objects and validate those. If the caller delivers strings instead, choose an explicit conversion policy and test it. Do not assume json.loads has converted domain types.
For deliberately permissive ingestion, enumerate the coercions you accept. In our model, lax Python admitted an integer string, Boolean and integral float for the retry count. A consumer that must distinguish those inputs should not silently adopt that policy. Pydantic's conversion table is the primary reference for type-specific behavior; your own model and installed version are the test target.
Custom validators introduce another variable. A historical upstream discussion about model validators and JSON mode illustrates why a minimal model is not enough to establish your application's behavior. Our fixture has no custom validators and does not reproduce or claim an unresolved defect from that discussion.
Run the complete acceptance fixture
The code below is the complete source tested through Code Interpreter Tools. It uses only synthetic payloads, performs no production request and prints versions, acceptance outcomes and field-level error types.
If Aident is not installed, tell your agent: Follow https://aident.ai/SETUP.md. Ask it to discover Code Interpreter Tools, inspect the current execution Action and preflight this fixture. Pass the complete code rather than substituting a local shell or running a live job. Check the terminal result for PASS, assertions: 40, the printed versions and an empty error/stderr. If the package is missing or the outcomes differ, stop and inspect that environment; do not disable assertions to manufacture success.
What this result does not establish
This is an acceptance-policy experiment on one model and runtime, not a throughput benchmark, security audit or claim about all Pydantic types. It excludes custom validators, framework request handling, transport limits, duplicate JSON names, authorization and actual downstream execution. Accepted values still need application-specific constraints, such as a permitted date range or retry limit.
Avoid logging real rejected payloads just to imitate the fixture. Error details can contain sensitive input. Use synthetic cases for reproduction and apply your application's data-handling rules to operational diagnostics.
Re-run the matrix when Pydantic or Python changes, when you add validators, or when a caller switches between raw JSON and already-decoded Python objects. Add your own field constraints and positive typed-input cases before relying on the result.
Put the test before the job
Use this fixture to make input acceptance explicit, then keep validation separate from the operation that consumes the values. Code interpreter versus AI-agent sandbox explains the execution boundary. For the complementary question of duplicate names before object construction, use the Python JSON acceptance test; Pydantic type strictness is not that lexical policy.
Run the strict input acceptance fixture before adapting the model to a real consumer.
Sources and method
Primary references are linked at the relevant claims above. Original evidence is the complete 12-payload fixture, run October 2, 2026 on Python 3.13.14/Pydantic 2.13.4 through Aident Loadout Code Interpreter: three input policies, 40 passing assertions, no custom validators or production payloads. The historical upstream discussion is context, not a current defect claim. Search visibility, user setup and commercial outcomes are not measured by this experiment.
About the author

Todd Mercer
Todd Mercer is a senior engineer with more than 10 years of experience working on social media platforms and large-scale ordering systems. He graduated from the University of Illinois Urbana-Champaign in computer science. At Aident, he covers technical issues, developer tools, and technology topics.



The one tool
for every tool
your agent needs.
Give any AI agent real capabilities in seconds. Connect 27,000+ tools once, skip the setup headache, and let your agents execute.


