Skip to content

Strange ValidationErrors with ≥2 nested instances of model with customized __get_pydantic_core_schema__ (embedded schemas differ, field ordering dependent) #10160

@DylanLukes

Description

@DylanLukes

Initial Checks

  • I confirm that I'm using Pydantic V2

Description

I have a model which validates from and serializes to a string. The Thing model in the example code below is intended to be a reduced/simplified case with trivial validation logic, the original is a bit more involved (actual parser, etc).

Note: some code elided in this explanation, full example code is below at the bottom of this post.

class Thing(BaseModel):
    value: str

    @classmethod
    def _validate(cls, value: str) -> Thing: ...
    
    @classmethod
    def __get_pydantic_core_schema__(cls, source, handler, /) -> CoreSchema:
        string_schema = cs.chain_schema([
            cs.str_schema(),
            cs.no_info_plain_validator_function(cls._validate)
        ])
        string_serializer = cs.plain_serializer_function_ser_schema(lambda t: t.value)

        return cs.json_or_python_schema(
            python_schema=cs.union_schema([string_schema, handler(cls)]),  # validate from string, or fall back on default
            json_schema=string_schema,
            serialization=string_serializer
        )

What I'd expect is that if I nest Thing inside another model, I can validate that model from string values. This works swimmingly when there is only one field of type Thing nested inside another model:

class Foo(BaseModel):
    thing1: Thing

foo = Foo.model_validate({
    "thing1": "hello"
})

I'd then expect this to work identically when I add a second field of type Thing... however, as soon as there are multiple, validation fails:

class Bar(BaseModel):
    thing1: Thing
    thing2: Thing

bar = Bar.model_validate({
    "thing1": "hello",
    "thing2": "world"
})
pydantic_core._pydantic_core.ValidationError: 1 validation error for Bar
thing2
  Input should be a valid dictionary or instance of Thing [type=model_type, input_value='world', input_type=str]
    For further information visit https://errors.pydantic.dev/2.8/v/model_type

Example Code

from __future__ import annotations

from pydantic import BaseModel, GetCoreSchemaHandler
from pydantic_core import CoreSchema
from pydantic_core import core_schema as cs


class Thing(BaseModel):
    value: str

    @classmethod
    def _validate(cls, value: str) -> 'Thing':
        if value != value.lower():
            raise ValueError("Value must be lowercase")
        return Thing.model_construct(value=value)

    @classmethod
    def __get_pydantic_core_schema__(cls, source: type[BaseModel], handler: GetCoreSchemaHandler, /) -> CoreSchema:
        string_schema = cs.chain_schema([
            cs.str_schema(),
            cs.no_info_plain_validator_function(cls._validate)
        ])
        string_serializer = cs.plain_serializer_function_ser_schema(lambda t: t.value)

        return cs.json_or_python_schema(
            python_schema=cs.union_schema([string_schema, handler(cls)]),
            json_schema=string_schema,
            serialization=string_serializer
        )


class Foo(BaseModel):
    thing1: Thing


class Bar(BaseModel):
    thing1: Thing
    thing2: Thing


# OK
foo = Foo.model_validate({
    "thing1": "hello"
})
print(foo)

# Errors
bar = Bar.model_validate({
    "thing1": "hello",
    "thing2": "world"
})
print(bar)

Python, Pydantic & OS Version

pydantic version: 2.8.2
        pydantic-core version: 2.20.1
          pydantic-core build: profile=release pgo=true
                 install path: 
.../lib/python3.12/site-packages/pydantic
               python version: 3.12.3 (main, Jun  4 2024, 16:53:26) [Clang 
15.0.0 (clang-1500.3.9.4)]
                     platform: macOS-14.4.1-arm64-arm-64bit
             related packages: typing_extensions-4.12.1 pydantic-settings-2.3.1 
mypy-1.10.1 pyright-1.1.366 fastapi-0.111.0
                       commit: unknown

Metadata

Metadata

Assignees

Labels

bug V2Bug related to Pydantic V2pendingIs unconfirmed

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions