Pydantic v2 'MockValSer object cannot be converted to SchemaSerializer': The Real Cause
The model validates fine. Instances construct, field access works, everything looks healthy. Then you call .model_dump() and Pydantic throws an error from somewhere deep in its Rust core that tells you nothing about your code:
TypeError: 'MockValSer' object cannot be converted to 'SchemaSerializer'
There is no line in your model that mentions MockValSer. There is no obvious culprit. So you start bisecting: comment out a field, run again, uncomment, comment out another, for an hour, because the error message hides the one thing you need to know. This is an open, unresolved issue in Pydantic v2 with a long comment thread and no clean upstream fix at the time of writing, which is exactly why it deserves a proper explanation rather than a copied one-liner.
MockValSer is the placeholder Pydantic installs when a model's schema could not be fully built, almost always because of an unresolved forward reference. Serialization then fails because the real serializer was never constructed.
- Call
Model.model_rebuild(force=True)after every forward-referenced class is defined, so the mock is replaced with the real serializer. - Also fix any field default that is invalid for its declared type (for example an empty tuple default on a model-typed field), which can leave the schema in the same half-built state.
- Do not paper over it with
exclude_unset=True. That only avoids touching the broken field; the schema is still wrong.
What MockValSer actually is
When Pydantic builds a model, its Rust core (pydantic-core) compiles two things for the model: a validator and a serializer. If, at the moment the class is defined, Pydantic cannot fully resolve the schema, most commonly because a field refers to a type that is not defined yet, it does not fail loudly. Instead it installs a placeholder object, a mock validator/serializer, and marks the model as not fully built. The idea is that you will call model_rebuild() later, once the missing type exists, and the mock gets swapped for the real thing.
Validation can limp along with the mock in some cases, which is why the model appears to work. Serialization cannot. When you call model_dump(), pydantic-core reaches for the real SchemaSerializer and finds the MockValSer placeholder instead, and it cannot convert one to the other. Hence the message: 'MockValSer' object cannot be converted to 'SchemaSerializer'. The error is not about serialization at all; it is telling you the schema was never finished being built.
The two triggers
Trigger 1: an unresolved forward reference
This is the usual cause. You reference a type by name (a string, or under from __future__ import annotations where every annotation becomes a string), and that type is defined later, in another module, or is mutually recursive with the current model:
from pydantic import BaseModel
class Node(BaseModel):
value: int
children: "list[Node]" = [] # forward reference to Node itself
# Node's schema references "Node" before it is fully defined.
# Pydantic may install a mock serializer here.
n = Node(value=1) # this can succeed
n.model_dump() # this raises the MockValSer TypeError
The self-reference or cross-module reference means that at class-definition time, Pydantic cannot resolve Node to a finished schema, so it defers with a mock.
Trigger 2: a field default that is invalid for its type
The open issue also implicates a subtler pattern: a field typed as a BaseModel (or a container of one) given a default that does not match, such as an empty tuple on a field that should hold a model instance. When Pydantic tries to build the schema and the default cannot be reconciled with the declared type, the schema build can fall back to the same half-built, mock-installed state, and you get the identical error at model_dump() time even though no forward reference is obviously involved.
from pydantic import BaseModel, Field
class Inner(BaseModel):
x: int
class Outer(BaseModel):
inner: Inner = Field(default=()) # empty tuple is not a valid Inner
The lesson from both triggers is the same: anything that stops the schema from being fully built leaves a mock behind, and the mock only bites at serialization.
The fix
Step 1: rebuild the model after all referenced classes exist
Call model_rebuild() once every class the model refers to has been defined. For a self-referential or mutually recursive model, that means after the class body (and after its partner, for mutual recursion). Use force=True so Pydantic rebuilds even if it thinks the model is already complete:
class Node(BaseModel):
value: int
children: "list[Node]" = []
Node.model_rebuild(force=True) # resolve the forward ref, replace the mock
n = Node(value=1, children=[Node(value=2)])
print(n.model_dump())
# expected: {'value': 1, 'children': [{'value': 2, 'children': []}]}
For two mutually recursive models, define both, then rebuild both:
class A(BaseModel):
b: "B | None" = None
class B(BaseModel):
a: "A | None" = None
A.model_rebuild(force=True)
B.model_rebuild(force=True)
Step 2: fix any default that does not match its field type
Give each field a default that is actually valid for its declared type. A model-typed field should default to None (with the type marked Optional) or to a proper factory, never to an unrelated empty container:
from typing import Optional
from pydantic import BaseModel, Field
class Inner(BaseModel):
x: int = 0
class Outer(BaseModel):
inner: Optional[Inner] = None # valid default
items: list[Inner] = Field(default_factory=list) # not default=()
Use default_factory=list for a mutable collection default rather than a literal, which is both correct Pydantic practice and avoids the shared-mutable-default class of bug entirely.
Step 3: rebuild at the right place in module import order
If the referenced class lives in another module, the rebuild has to happen after that module is imported, not at definition time. Put the model_rebuild(force=True) call at the bottom of the module once the imports that supply the referenced types have run, or in the package's __init__ after both modules are loaded. The failure mode people miss is calling model_rebuild() too early, before the type it needs is importable, which leaves the mock in place.
What people get wrong: hiding it with exclude_unset
The most common "fix" you will find is to call model_dump(exclude_unset=True) or exclude_none=True and declare victory because the error went away. It went away because those options skip serializing the field whose serializer is broken; if you never set the recursive or model-typed field, the mock never gets exercised. This is not a fix. The schema is still half-built, the model's serializer is still a mock, and the moment a request actually populates that field, or you dump without the exclusion, the TypeError is back, now in production instead of in a test. Treat a passing exclude_unset dump as a diagnosis confirming the field is broken, not as a resolution.
The second mistake is assuming this is a bug in your serialization logic and rewriting your model_dump call, custom serializers, or field validators. None of that touches the cause, because the cause is upstream at schema build time. If you build LLM tools or APIs that lean on Pydantic for structured outputs and JSON schema, this bites especially hard, because those schemas are frequently recursive and defined across modules.
Verification
Confirm the model is genuinely built, not just quiet. Pydantic exposes whether the model finished building:
print(Node.__pydantic_complete__) # expected: True after a successful rebuild
A True here means the mock has been replaced with a real serializer. Then prove serialization works on a fully populated instance, including the previously broken field, and round-trips:
n = Node(value=1, children=[Node(value=2, children=[])])
data = n.model_dump()
assert Node(**data).model_dump() == data # round-trip holds
print("ok")
If __pydantic_complete__ is still False after your rebuild, the type it needs is still not resolvable, and you are calling rebuild too early or from the wrong scope.
When it is still broken
If model_rebuild(force=True) does not clear it, the referenced type is genuinely not in scope where you call rebuild; pass the namespace explicitly with the _types_namespace argument, or move the call to where both types are importable. If you use from __future__ import annotations, remember that every annotation in the module becomes a string forward reference, which widens the surface for this bug, so rebuilding the affected models becomes mandatory rather than occasional. And because this is an open upstream issue, keep an eye on the Pydantic changelog: some variants of the crash have been narrowed over releases, so upgrading to the current stable pydantic and pydantic-core is worth trying, but do not assume the upgrade alone fixes it without the rebuild call in place.
Frequently asked questions
- What does 'MockValSer object cannot be converted to SchemaSerializer' mean?
- MockValSer is a placeholder serializer that Pydantic installs when a model's schema could not be fully built, usually because a field refers to a type that was not defined yet, an unresolved forward reference. Validation may still work, but model_dump() needs the real SchemaSerializer and finds the mock instead, so it raises this TypeError. The error is really telling you the schema was never finished.
- How do I fix the MockValSer error in Pydantic v2?
- Call Model.model_rebuild(force=True) after every class the model references has been defined, which replaces the mock with the real serializer. Also fix any field default that is invalid for its declared type, such as an empty tuple on a model-typed field, since that leaves the schema half-built in the same way. Verify with Model.__pydantic_complete__ being True.
- Does exclude_unset=True fix the MockValSer error?
- No, it only hides it. exclude_unset or exclude_none makes model_dump skip the field whose serializer is broken, so if you never populate that field the mock is never exercised. The schema is still half-built, and the moment the field is populated or you dump without the exclusion the TypeError returns, now likely in production. Treat a passing exclude_unset dump as confirmation of which field is broken, not as a fix.
- Why does model_rebuild not clear the error sometimes?
- Because the referenced type is still not resolvable in the scope where you call model_rebuild. Make sure the module supplying the referenced type has been imported first and call the rebuild after it, or pass the namespace explicitly via the _types_namespace argument. Calling model_rebuild before the needed type is importable leaves the mock in place and __pydantic_complete__ stays False.