pydantic/pydantic · error · PydanticSerializationUnexpectedValue

Expected ` ` but got ` ` with value `' '` - serialized…

Error message

Expected `{tp}` but got `{type(ip)}` with value `'{ip}'` - serialized value may not be as expected.

What it means

PydanticSerializationUnexpectedValue raised inside ser_ip, the serializer pydantic attaches to ipaddress types (IPv4Address, IPv6Network, IPv4Interface, etc.). It fires at serialization time when model_dump(mode='json') (or any SerializationInfo path) calls ser_ip with a value that is neither an instance of the expected ip type (tp) nor a str. The serializer's contract is 'either the right IP object or a string fallback'; anything else means the field was assigned a wrong type, so serialization would produce an unexpected value.

Solutions

  1. Always construct the model via Model(**data) or Model.model_validate(data) so the IP field runs through IP_VALIDATOR_LOOKUP and becomes a real IP type.
  2. If using model_construct(), pre-coerce the value to the ipaddress type (ipaddress.ip_address(raw)) before assignment.
  3. Make the field type str | IPv4Address if arbitrary strings must be accepted, or store the raw value in a separate str field.
  4. In a custom serializer/validator, ensure the returned value is an instance of the declared IP type or a str.

Example fix

// before
m = MyModel.model_construct(ip=12345)
m.model_dump_json()  # ser_ip: Expected IPv4Address but got int

// after
import ipaddress
m = MyModel.model_construct(ip=ipaddress.ip_address('1.2.3.4'))
m.model_dump_json()
Defensive patterns

Strategy: validation

Validate before calling

import ipaddress

def coerce_ip(value, ip_type):
    if isinstance(value, ip_type) or isinstance(value, str):
        return value
    # validate eagerly so the error is a ValidationError, not a serialization surprise
    return ip_type(value)

# data['ip'] = coerce_ip(data['ip'], ipaddress.IPv4Address) before model_validate

Type guard

import ipaddress

def is_ip_or_str(value, ip_type) -> bool:
    return isinstance(value, (ip_type, str))

Try / catch

from pydantic_core import PydanticSerializationUnexpectedValue

try:
    obj.model_dump_json()
except PydanticSerializationUnexpectedValue as e:
    # re-validate the offending field via model_validate, then retry serialization
    obj = MyModel.model_validate(obj.model_dump())
    raise

Prevention

When it happens

Trigger: Assigning an int, float, dict, or unrelated object to a model field typed IPv4Address and then calling model_dump_json(); building a model with model_construct() (skipping validation) and putting a non-IP value in an IP field; a custom validator that returns a non-IP, non-str value; serializing in 'json' mode when the field was populated by external code that bypassed pydantic validation.

Common situations: Using model_construct() for performance and feeding raw data straight from a DB row whose column is a string that is not a valid IP; mixing validated and unvalidated construction paths; a field declared IPv4Address but populated by an ORM relationship returning a numeric ID; concurrent mutation of the field after validation.

Related errors


AI-assisted analysis of pydantic/pydantic@cc13d1b8c9 (2026-08-11). Data as JSON: /api/errors/869b5d1cb6e4145d. Report an issue: GitHub.

Appendix: source

Thrown at pydantic/_internal/_generate_schema.py:569

                enum_type,
                metadata={'pydantic_js_functions': [get_json_schema_no_cases]},
            )

    def _ip_schema(self, tp: Any) -> CoreSchema:
        from ._validators import IP_VALIDATOR_LOOKUP, IpType

        ip_type_json_schema_format: dict[type[IpType], str] = {
            IPv4Address: 'ipv4',
            IPv4Network: 'ipv4network',
            IPv4Interface: 'ipv4interface',
            IPv6Address: 'ipv6',
            IPv6Network: 'ipv6network',
            IPv6Interface: 'ipv6interface',
        }

        def ser_ip(ip: Any, info: core_schema.SerializationInfo) -> str | IpType:
            if not isinstance(ip, (tp, str)):
                raise PydanticSerializationUnexpectedValue(
                    f"Expected `{tp}` but got `{type(ip)}` with value `'{ip}'` - serialized value may not be as expected."
                )
            if info.mode == 'python':
                return ip
            return str(ip)

        return core_schema.lax_or_strict_schema(
            lax_schema=core_schema.no_info_plain_validator_function(IP_VALIDATOR_LOOKUP[tp]),
            strict_schema=core_schema.json_or_python_schema(
                json_schema=core_schema.no_info_after_validator_function(tp, core_schema.str_schema()),
                python_schema=core_schema.is_instance_schema(tp),
            ),
            serialization=core_schema.plain_serializer_function_ser_schema(ser_ip, info_arg=True, when_used='always'),
            metadata={
                'pydantic_js_functions': [lambda _1, _2: {'type': 'string', 'format': ip_type_json_schema_format[tp]}]
            },
        )

View on GitHub (pinned to cc13d1b8c9)