Pydantic Strict Mode: JSON and Python Inputs Are Not Equivalent

Pydantic Strict Mode: JSON and Python Inputs Are Not Equivalent

Todd Mercer

A lilac curved plane admits a tangerine ribbon beside a blue fold and peach oval, representing distinct input policies.

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 "2"

Reject

Reject

Accept

Retry count true

Reject

Reject

Accept

Retry count 2.0

Reject

Reject

Accept

Retry count 2.5

Reject

Reject

Reject

Tuple elements "1", "3"

Reject

Reject

Accept

Three elements for a two-element tuple

Reject

Reject

Reject

Impossible date 2026-02-30

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.

import json
import platform
from datetime import date
from uuid import UUID

import pydantic
from pydantic import BaseModel, ConfigDict, ValidationError


class Job(BaseModel):
    model_config = ConfigDict(extra="forbid")
    run_date: date
    job_id: UUID
    retries: int
    window: tuple[int, int]


base = {
    "run_date": "2026-10-02",
    "job_id": "12345678-1234-5678-1234-567812345678",
    "retries": 2,
    "window": [1, 3],
}
cases = [
    ("valid_wire", {}, [True, False, True]),
    ("integer_string", {"retries": "2"}, [False, False, True]),
    ("integer_boolean", {"retries": True}, [False, False, True]),
    ("integer_float", {"retries": 2.0}, [False, False, True]),
    ("fractional_integer", {"retries": 2.5}, [False, False, False]),
    ("tuple_integer_strings", {"window": ["1", "3"]}, [False, False, True]),
    ("tuple_wrong_length", {"window": [1, 3, 5]}, [False, False, False]),
    ("invalid_calendar_date", {"run_date": "2026-02-30"}, [False, False, False]),
    ("midnight_datetime", {"run_date": "2026-10-02T00:00:00"}, [False, False, True]),
    ("invalid_uuid", {"job_id": "not-a-uuid"}, [False, False, False]),
    ("unknown_field", {"unexpected": "present"}, [False, False, False]),
    ("null_integer", {"retries": None}, [False, False, False]),
]
policies = ["strict_json", "strict_decoded_python", "lax_decoded_python"]
rows = []
assertions = 0
for name, patch, expected in cases:
    data = {**base, **patch}
    wire = json.dumps(data)
    decoded = json.loads(wire)
    results = []
    for policy in policies:
        try:
            if policy == "strict_json":
                result = Job.model_validate_json(wire, strict=True)
            else:
                result = Job.model_validate(decoded, strict=policy.startswith("strict"))
            results.append({"accepted": True, "types": {k: type(getattr(result, k)).__name__ for k in base}})
        except ValidationError as exc:
            results.append({"accepted": False, "errors": [{"field": list(e["loc"]), "type": e["type"]} for e in exc.errors(include_url=False)]})
    for actual, want in zip(results, expected):
        assert actual["accepted"] == want, (name, results, expected)
        assertions += 1
    rows.append({"case": name, "accepted": expected, "results": results})
typed = {**base, "run_date": date(2026, 10, 2), "job_id": UUID(base["job_id"]), "window": (1, 3)}
job = Job.model_validate(typed, strict=True)
assert job.run_date == date(2026, 10, 2)
assert job.job_id == UUID(base["job_id"])
assert job.retries == 2
assert job.window == (1, 3)
assertions += 4
print(json.dumps({"status": "PASS", "python": platform.python_version(), "pydantic": pydantic.__version__, "policies": policies, "cases": rows, "assertions": assertions, "scope": "synthetic wire-input acceptance; no custom validators, performance, production payloads, or security benchmark"}, indent=2))

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

Illustrated editorial avatar of Todd Mercer

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.

Home

Home

Home

Integrations

Integrations

Integrations

Vault

Vault

Vault

Audit

Audit

Audit

Arana Grande

Arana Grande

Arana Grande

Free

Free

Free

30-day audit summary

30-day audit summary

30-day audit summary

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

View Audit

View Audit

View Audit

Loadout usage

Loadout usage

Loadout usage

617 action calls in the last 30 days

617 action calls in the last 30 days

617 action calls in the last 30 days

May 19 - Jun 17

May 19 - Jun 17

May 19 - Jun 17

10 active days

10 active days

10 active days

Less

Less

Less

More

More

More

Recent activity

Recent activity

Recent activity

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Shopify

Shopify

Shopify

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Success

Success

Success

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Success

Success

Success

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Failed

Failed

Failed

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Connected integration coverage

Connected integration coverage

Connected integration coverage

162

162

162

of 753 accessible connected

of 753 accessible connected

of 753 accessible connected

Callable actions

Callable actions

Callable actions

1,126

1,126

1,126

Vault credentials

Vault credentials

Vault credentials

8

8

8

Explore what's possible

Explore what's possible

Explore what's possible

See all Integrations

See all Integrations

See all Integrations

Google Ads

Google Ads

Google Ads

All available Goolge Ads tools via...

All available Goolge Ads tools via...

All available Goolge Ads tools via...

X (twitter)

X (twitter)

X (twitter)

All available X tools via...

All available X tools via...

All available X tools via...

Github

Github

Github

All available Github tools via...

All available Github tools via...

All available Github tools via...

Notion

Notion

Notion

All available Notion tools via...

All available Notion tools via...

All available Notion tools via...

Slack

Slack

Slack

All available Slack tools via...

All available Slack tools via...

All available Slack tools via...

Firecrawl

Firecrawl

Firecrawl

All available Firecrawl tools via...

All available Firecrawl tools via...

All available Firecrawl tools via...

753 integrations are available for loadouts.

753 integrations are available for loadouts.

753 integrations are available for loadouts.

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.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.