We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Data Modelling and Invariants step 20 of 25
Parse at the boundary, dataclasses in the core
Once BaseModel is your domain type, your business logic is coupled to a
serialization library’s field semantics, its upgrade cadence and its performance
envelope. Once dict[str, Any] is your domain type, you pay for the missing
schema in incidents instead. The discipline that avoids both is one sentence:
Parse once at the edge, then work with frozen dataclasses.
Three shapes, and what each costs
Measured on 3.14.6, three int fields, min-of-3, construct / ==:
| shape | numbers | what you get |
|---|---|---|
pydantic.BaseModel |
354.8 ns / 224.9 ns |
full feature set, model_dump, JSON Schema — and your domain now imports pydantic |
pydantic.dataclasses.dataclass |
248.5 / 99.0 |
dataclass ergonomics + validation, but the docs are explicit that model_dump and JSON-Schema are not available; you need a TypeAdapter for those |
TypeAdapter over a plain stdlib dataclass |
dataclass speed in the core | validation and schema at the boundary, domain module dependency-free |
| hand-written parser | fastest, most code | zero dependency; right for a small, stable schema |
Compare against a plain dataclass at 43.5 ns / 45.8 ns and the shape of the decision is clear: pay for validation once, where untrusted bytes arrive, and never again on the ten million internal constructions that follow.
Caveats worth carrying: a parameterised generic pydantic dataclass is treated
as [Any] unless you go through TypeAdapter; __post_init__ runs between
before- and after-validators, which is not where most people assume; and pydantic
coerces by default — "3" becomes 3 unless you ask for strict mode. That
last one is the difference between a boundary that catches a broken producer and
one that quietly repairs it and hides the outage.
This exercise builds the fourth row — the hand-written parser — because it makes
the contract explicit. Everything a TypeAdapter does for you, you are about to
do by hand once, so you can recognise it when a library does it.
The signature is the pattern
def parse_event(raw: object) -> Event: ...
object in — not dict[str, Any], which would let Any leak into every
expression that touches the input. A fully-typed frozen dataclass out. Nothing
in between is Any, and no partially-valid object is ever constructed: you
either get an Event that satisfies every invariant or you get an exception.
Your task
Implement parse_event(raw: object) -> Event, raising only InvalidEvent
(never a KeyError, TypeError or AttributeError from a failed cast), with
these rules and these exact messages:
-
not a mapping →
InvalidEvent("<root>", f"expected object, got {type(raw).__name__}"); -
any key that is not a field name →
InvalidEvent(<first such key, sorted>, "unknown key"), checked before anything else. Silently ignoring unknown keys is how a producer’s renamed field becomes a month of missing data; -
a missing key →
InvalidEvent(key, "missing"); -
a wrong type →
InvalidEvent(key, f"expected <type>, got {type(value).__name__}"), with<type>one ofstr,int,float,list,object; -
an unknown enum value →
InvalidEvent("kind", f"unknown kind {value!r}"); -
tagsmust be alistofstrand is stored as a tuple; -
metais optional: absent ornull→None; otherwise an object ofstr -> str, orInvalidEvent("meta", "expected object of str -> str").
Strict, not coercive. "3" is not a valid count. 1 is not a valid
ratio — an int where a float is declared is exactly the kind of drift a
boundary exists to catch. And True is not a valid count, even though
isinstance(True, int) is True: check bool first, or every boolean in
your input silently becomes 0 or 1.
solve(raw) returns {"error": "", "event": {...}} with the event rendered as
plain JSON types, or {"error": "<message>", "event": None}.
Note what the Event module needs to import to do all this: dataclasses,
enum, collections.abc. That is the entire point.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.