model_dump and model_dump_json

Getting data back out: the two modes, the arguments that shape the output, and the mistake that raises TypeError.

Overview

Two methods, and the difference that matters

Validation brings data in. Sooner or later you have to send it back out, as a dictionary for Python code or as JSON for a client. Pydantic has two methods for this, and they look similar enough that people use the wrong one.

The first, model_dump, gives you a dictionary of Python values. The second, model_dump_json, gives you a JSON string. The difference matters because some Python values, such as dates and decimals, are not JSON. Pass a dumped dictionary straight to the standard json module and you can hit a TypeError.

This article explains the difference between the two methods and when each one is right. It covers that TypeError and how to avoid it. It then shows how to shape the output, choosing which fields to include and which to leave out. There are three exclusion options that look alike, such as leaving out unset fields, default fields and None values, and the article separates them clearly. By the end you will know how to produce exactly the output a caller expects, and how to do it with the least code.

You will use these two methods in almost every project, so small details add up quickly.

Each option is shown with the exact output it produces.

model_dump() returns a dict of Python objects. A date field is a date. A Decimal is a Decimal. An enum member is the member.

model_dump_json() returns a JSON string, converting all to a thing JSON can carry.

The rule of thumb: dump for inside, dump_json for outside. If the data is going to another function, a template, a test assertion, use model_dump(). If it's going into an HTTP response, a queue, or a file, use model_dump_json().

There's a third form worth knowing: model_dump(mode="json") gives you a dict whose values are already JSON-matching. That is what you want when a thing else will do the encoding. A framework, a JSON logger, an SDK that takes a dict.

In Pydantic v1 these were .dict() and .json(). They still exist in v2 and warn. They're all over the internet.

Worth knowing

model_dump() keeps Python objects; model_dump_json() produces JSON text. Use the second for anything leaving the process.
json.dumps(m.model_dump()) raises TypeError on dates and decimals. Use model_dump_json(), or model_dump(mode="json") if you need the dict.
include and exclude take sets or nested dicts. Once one entry needs to reach inside, every entry becomes a key.
exclude_unset is the one for PATCH: it emits only what the caller supplied, so an explicit null survives and an omission stays omitted.
exclude_none is not the same thing — it drops explicit nulls too, which throws away the instruction to clear a field.
Field(exclude=True) keeps a value out of every dump permanently. SecretStr additionally hides it from repr and logs.

model_dump and model_dump_json: Getting Data Back Out

Two methods, a few filters, and the one mistake that raises TypeError.

Two methods, two purposes

model_dump() gives Python objects for use inside your program. model_dump_json() gives text for leaving it.

example_01.pyPydantic
Output

The TypeError everybody meets once

Passing model_dump() to json.dumps fails, because the dict still holds real Python objects.

example_02.pyPydantic
Output

Choosing what comes out

include and exclude pick fields, and reach into nested models with a dict.

example_03.pyPydantic
Output

Dropping what was never set

Three filters that matter for updates: exclude_unset, exclude_defaults and exclude_none.

example_04.pyPydantic
Output

Excluding a field permanently

Field(exclude=True) keeps a value out of every serialisation, which is what you want for anything secret.

example_05.pyPydantic
Output

Round tripping

A dump is valid input again — but only if you did not filter it. That is worth checking rather than assuming.

example_06.pyPydantic
Output

A dump is valid input to the same model:

Module.model_validate(m.model_dump()) == m

That is truly handy for copying, for caching, and for tests.

It stops being true the moment you filter. model_dump(exclude={"minutes"}) makes a dict missing a needed field. Checking it raises. Worth remembering when a filtered dump is being stored and later read back. The filter that made the output tidy also made it un-reloadable.

Round tripping through JSON works too, and is exact for the types Pydantic knows.

The TypeError

Everybody meets this once:

json.dumps(m.model_dump())
# TypeError: Object of type date is not JSON serializable

It's confusing because the model obviously supports JSON, and the error blames a date.

model_dump() on purpose didn't convert anything. It handed you real Python objects. json.dumps doesn't know what to do with a date.

Three correct forms: model_dump_json() if you want the string, json.dumps(model_dump(mode="json")) if a thing else must do the encoding, or model_dump(mode="json") alone if you need the dict.

Shaping the output

include and exclude select fields. A set names top-level fields. A dict reaches into nested models. The two spellings don't mix in one literal. Once anything needs to reach inside, each entry turns a key with True.

For lists of models there's a special key, "__all__", applying a pick to each item.

Both are handy for dropping one obviously-inner field. Beyond that, a split output model reads better, appears right in the schema. Doesn't need anyone to trace an exclusion formula to work out what an endpoint returns.

The three exclusions that look similar

These get confused constantly, and the gap is the whole point of the update module.

exclude_unset=True omits fields the caller never supplied. Fields by name sent, *including clear nulls*, are kept.

exclude_defaults=True omits fields whose value equals their default, whether supplied or not.

exclude_none=True omits each field that is None. But It got that way.

For a PATCH endpoint, exclude_unset is the correct one and the other two are wrong.

Consider a client sending {"summary": null} meaning "clear the summary". With exclude_unset the output is {"summary": None}. The instruction lives. With exclude_none the output is {}. The instruction is gone. The summary stays as it was. That is a bug users report as "the clear button doesn't work", and it's one arg away.

exclude_defaults has a narrower use: making a minimal config file, or a payload where anything unspecified should fall back to the receiver's defaults.

Keeping secrets out

Two methods, for two other exposures.

Field(exclude=True) keeps a field out of each output. It's still on the object and still accessible. It simply never appears in a dump. This is right for a password hash, an inner id, or anything the model needs and consumers must not see.

SecretStr handles a other risk: the value appearing in a repr, a log line or a traceback. It displays as ********** and needs .get_secret_value() to read. This Makes each access deliberate and easy to grep for.

They're complementary. A token that must neither be logged nor serialised wants both.

The safest structure of all is a split output model that simply doesn't have the field. You can't leak what isn't there. No future refactor can accidentally drop a flag.

Nested behaviour

all recurses. model_dump() on a model holding models returns nested plain dicts, and model_dump_json() makes nested JSON.

One thing to check when a nested model is a *other* class than you expect. Output follows the field's declared type. If you assign a subclass instance to a field annotated with the parent, by default the extra fields aren't serialised. The model serialises per to what it promised rather than what it happens to hold.

That action is deliberate — it stops a subclass leaking fields through an API that documented the parent — and it surprises people who expected the subclass's data. SerializeAsAny opts out where you truly want the richer output.

Warnings

Pydantic will warn when output meets a thing it didn't expect. Often a field holding a value that doesn't match its annotation. This Happens when validate_assignment is off and a thing assigned freely.

Those warnings are worth listening to rather than silencing. They mean the object's real contents have diverged from what the model claims. The serialised output may not be what the schema promises.

Rounding trips and warnings

Pydantic emits a warning when output meets a value that doesn't match its field's annotation. That happens when validate_assignment is off and a thing assigned freely, leaving the object's contents at odds with what the model claims.

Those warnings are worth reading rather than filtering. They mean the serialised output may not match the schema you publish. This Is a defect a consumer will find before you do.

If you see them regularly, the fix is upstream: turn on validate_assignment, or freeze the model so the divergence can't happen.

Subclasses and what gets emitted

If a field is annotated with a parent model and holds a subclass instance, the extra fields are not serialised by default. The model emits what it promised, not what it happens to hold.

That is deliberate, and it's a safety property. A subclass carrying inner fields can't leak them through an endpoint documented as returning the parent.

It also surprises people who expected the richer output. SerializeAsAny[Parent] opts out where you truly want what the object really is. And where you've met yourself that all a subclass might carry is safe to expose.

Context

Both dump methods accept a context dict. Serialisers can read it through their info arg.

That is the supported way to make output depend on a thing external without a global. A locale, a viewer's permissions, a feature flag:

m.model_dump(context={"role": "admin"})

Used sparingly it's the clean answer to "this field is only visible to some callers". Used heavily it makes output no one can predict from the model alone. Split output models are clearer.

Choosing between filtering and separate models

The recurring question in this module: exclude or a second model?

Use exclude for one or two obviously-inner fields. Here The shapes are else same and the intent is plain at the call site.

Use a split model when the gap is structural, when the same gap is needed in more than one place, or when the output is part of a public contract. A model is checked, appears right in the schema, can't be forgotten at a new call site. Is hopeless to get wrong by mistyping a field name in a set.

The exclusion set is the thing that quietly stops matching the model. A field renamed in the model and not in the exclusion set silently starts appearing in your public output, and nothing raises.

Performance

output happens in Rust and is fast. It isn't free. Two habits cost more than people expect.

Serialising more than you send. Building a full dump and then picking three keys out of it does the work for each field including nested trees. include does the same job without the waste.

Serialising the same object again and again. Inside a loop that renders one object per row, hoisting the dump out is the obvious fix and easy to miss.

Neither matters at small scale. Both are visible when a response holds thousands of models.

model_dump and model_dump_json: summary

model_dump() for Python objects, model_dump_json() for text, model_dump(mode="json") for a JSON-safe dict.

json.dumps(model_dump()) is the classic mistake. The dict is full of real objects.

exclude_unset for PATCH, and not exclude_none. This Discards the gap between "not mentioned" and "please clear this".

Field(exclude=True) and SecretStr for secrets, and a split output model when you want the promise rather than the setting.

A last habit

Look at what your model really emits, once, before it goes anywhere public.

One print(m.model_dump_json(indent=2)) shows you the keys, the casing, the formats. Anything present that shouldn't be. It's the same five-minute review as printing the schema. It catches a other set of problems. A secret that was never excluded, a nested field no one meant to expose, snake_case where the rest of the API is camelCase.

The output of a model is a contract with all who consumes it. It's worth having read it.

Mistakes people make with model_dump and model_dump_json

**json.dumps(model_dump()).** The classic. The dict holds real date and Decimal objects. The standard encoder refuses them. The error blames a date and the cause is the wrong method.

**exclude_none where exclude_unset was meant.** On an update endpoint this discards a caller's clear instruction to clear a field. It presents as "the clear button doesn't work" and is one arg away from correct.

Filtering a dump that is later reloaded. An unfiltered dump round-trips. A filtered one is missing needed fields and won't validate. This bites when the tidy output is stored and read back later.

Trusting an exclusion set to stay correct. Rename a field in the model, forget the exclusion set. A thing inner silently begins appearing in your public output. Nothing raises. A split output model can't fail this way.

Serialising more than you need. Building a full dump of a nested tree to pick three keys out of it does all the work for each field. include does the same job without it.

Skipping output warnings. They mean an object's contents no longer match what the model claims, often because a thing was assigned without validate_assignment. The published schema and the actual output have diverged. A consumer will find it first.

The two mistakes

If only two things live from this module, make them these.

json.dumps(model_dump()) raises, because the dict is full of real Python objects. Use model_dump_json(), or mode="json" when a thing else does the encoding.

exclude_none isn't exclude_unset. The first throws away a caller's clear instruction to clear a field. The second preserves it. On a PATCH endpoint that gap is a bug users report and no one can copy.

Questions people ask about model_dump and model_dump_json

What is the difference between model_dump and model_dump_json? model_dump returns a Python dict, while model_dump_json returns a JSON string. The dict may hold objects like datetime that json.dumps cannot handle.

Why does json.dumps(m.model_dump()) raise a TypeError? The dict contains values like datetimes, UUIDs or Decimals. Use model_dump(mode="json") or model_dump_json() to get JSON-safe output.

How do I leave out fields? Use exclude={...}, include={...}, exclude_none=True, exclude_unset=True or exclude_defaults=True, depending on what you want removed.

What is the difference between exclude_unset and exclude_defaults? exclude_unset drops fields the caller never supplied. exclude_defaults drops fields whose value equals the default, even if the caller set it explicitly.

How do I include aliases in the output? Pass by_alias=True. The default output uses Python field names.

Can I round-trip a dump back into a model? Usually, with Model.model_validate(m.model_dump()). It breaks if you excluded required fields or used a custom serialiser that changes the shape.

Output as a contract

The output of a model is a contract, whether or not anybody wrote it down.

Somebody is parsing those keys. A thing depends on that date format. A client one spot assumes a field is present because it always has been. None of that is in a doc. It's in the action. It turns binding the moment anyone builds against it.

That is why the choices in this module deserve more care than they often get. Adding a found field changes each response the model feeds. Renaming a field breaks parsers. Changing an exclusion set alters what leaves your system, silently and without any test failing.

The hands-on habit is to treat a model that reaches the outside world as a published interface. Know what it emits, be deliberate about changing it. Prefer a split output model when the shape you send differs from the shape you hold. A dedicated class makes the contract clear, checkable and hard to alter by accident. Which is just what a contract should be.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Why does `json.dumps(m.model_dump())` raise TypeError on a date field?

  2. A PATCH client sends `{"summary": null}` to clear a field. Which filter preserves that?

  3. What does `Field(exclude=True)` do?

  4. Is `model_dump(exclude={"minutes"})` valid input to the same model?

Cheat sheet

model_dump and model_dump_json

Export a model with model_dump and model_dump_json, apply include and exclude filters, and avoid the TypeError most people hit first.

PYDANTIC · vizlearn.in/pydantic/model_dump_and_model_dump_json.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.