marshmallow-oneofschema polymorphic nested schemas: working versions (tested 2026-10-16)
A OneOfSchema discriminated union over nested dataclass schemas that loads and dumps polymorphic payloads, with tests covering type dispatch and its sharp edges.
passed6 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("marshmallow-oneofschema-polymorphic") (install), or see them on GitHub.
marshmallow==4.3.1 marshmallow-oneofschema==3.2.0 pytest==9.1.1
from dataclasses import dataclass
import marshmallow as ma
from marshmallow_oneofschema import OneOfSchema
@dataclass
class EmailTarget:
address: str
@dataclass
class SmsTarget:
number: str
class EmailTargetSchema(ma.Schema):
address = ma.fields.Email(required=True)
@ma.post_load
def make_target(self, data, **kwargs):
return EmailTarget(**data)
class SmsTargetSchema(ma.Schema):
number = ma.fields.Str(required=True)
@ma.post_load
def make_target(self, data, **kwargs):
return SmsTarget(**data)
class TargetSchema(OneOfSchema):
type_field = "kind"
type_schemas = {"email": EmailTargetSchema, "sms": SmsTargetSchema}
def get_obj_type(self, obj):
return {EmailTarget: "email", SmsTarget: "sms"}[type(obj)]
class NotificationSchema(ma.Schema):
id = ma.fields.Int(required=True)
target = ma.fields.Nested(TargetSchema, required=True)
import marshmallow as ma
import pytest
from marshmallow_oneofschema import OneOfSchema
from schemas import (
EmailTarget,
EmailTargetSchema,
NotificationSchema,
SmsTarget,
TargetSchema,
)
def test_load_dispatches_on_kind():
loaded = NotificationSchema().load(
{"id": 1, "target": {"kind": "email", "address": "[email protected]"}}
)
assert loaded["target"] == EmailTarget("[email protected]")
def test_dump_adds_kind_and_round_trips():
dumped = NotificationSchema().dump({"id": 2, "target": SmsTarget(number="+15555550100")})
assert dumped["target"] == {"number": "+15555550100", "kind": "sms"}
assert NotificationSchema().load(dumped)["target"] == SmsTarget("+15555550100")
def test_unknown_kind_is_reported_on_type_field():
with pytest.raises(ma.ValidationError) as exc:
TargetSchema().load({"kind": "carrier-pigeon", "address": "[email protected]"})
assert exc.value.messages == {"kind": ["Unsupported value: carrier-pigeon"]}
def test_default_get_obj_type_must_match_type_schemas_keys():
class NoGetObjType(OneOfSchema):
type_field = "kind"
type_schemas = TargetSchema.type_schemas
# the default get_obj_type returns the class name ("EmailTarget"), not the
# "email" key, and dump silently returns an error tuple instead of raising
result = NoGetObjType().dump(EmailTarget("[email protected]"))
assert result == (None, {"_schema": "Unsupported object type: EmailTarget"})
def test_type_field_remove_false_leaks_type_into_subschema():
class KeepsTypeField(OneOfSchema):
type_field = "kind"
type_field_remove = False
type_schemas = {"email": EmailTargetSchema}
with pytest.raises(ma.ValidationError) as exc:
KeepsTypeField().load({"kind": "email", "address": "[email protected]"})
assert exc.value.messages == {"kind": ["Unknown field."]}
def test_parent_unknown_option_does_not_reach_nested_schema():
data = {"id": 3, "target": {"kind": "email", "address": "[email protected]", "extra": 1}}
# unknown=EXCLUDE on the parent is not passed through fields.Nested
with pytest.raises(ma.ValidationError):
NotificationSchema().load(data, unknown=ma.EXCLUDE)
class Tolerant(ma.Schema):
id = ma.fields.Int(required=True)
target = ma.fields.Nested(TargetSchema, required=True, unknown=ma.EXCLUDE)
assert Tolerant().load(data)["target"] == EmailTarget("[email protected]")
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Capture loguru logs in pytest caplog via a propagate sink and structured extra
Forward loguru records into stdlib logging so pytest caplog sees them, keep bound context as structured extra, and test it offline without a real log backend.
FastAPI dependency_overrides tested offline with httpx ASGITransport
Override FastAPI dependencies (including nested ones) and drive the app in-process with httpx.ASGITransport, no network and no running server.
FastAPI metrics with prometheus_client scraped as text
Instrument FastAPI with prometheus_client using a per-app registry, label by route template to bound cardinality, and scrape the text exposition format offline through httpx.ASGITransport.
FastAPI metrics with prometheus-fastapi-instrumentator tested offline
Instrument a FastAPI app with the default prometheus-fastapi-instrumentator metrics, scrape /metrics through httpx.ASGITransport, and cover the route-template labels, untemplated grouping, self-scraping and shared-registry gotchas.
All marshmallow recipesAll marshmallow-oneofschema recipesAll pytest recipes