Tutorial: from record to production boundary¶
This tutorial grows one account contract from strict Python storage into an external API boundary. It introduces each operation separately so conversion, validation, output, errors, security, and schemas never blur into one pipeline.
1. Declare an immutable record¶
from talea import Spec, field
class Address(Spec):
line_1: str
city: str
postcode: str
country: str
class Account(Spec):
account_id: int
display_name: str
address: Address
labels: list[str] = field(default_factory=list)
Every field without a default or factory is required, even if its type accepts
None. Construction is keyword-only and strict. Nested values on this path are
already-valid Python objects:
address = Address(
line_1="1 Analytical Engine Way",
city="London",
postcode="SW1A 1AA",
country="GB",
)
account = Account(account_id=7, display_name="Ada", address=address)
Account(account_id="7", ...) fails: a string is not an integer. Likewise, a
dictionary is not an Address on the strict constructor path. This gives
application code a stable contract and keeps conversion attached to visible
external operations.
The factory runs once when labels is omitted, and each account receives a new
list. Spec field bindings are immutable; the nested list remains an ordinary
mutable Python value and will be revalidated at later boundaries.
2. Add aliases, constraints, and sensitive data¶
from typing import Annotated
from talea import Alias, MaxLength, MinLength, Sensitive, WriteOnly
class Credentials(Spec):
token: Annotated[str, Sensitive(), WriteOnly(), MinLength(16)]
class AccountCreate(Spec):
display_name: Annotated[
str,
Alias("displayName"),
MinLength(1),
MaxLength(80),
]
address: Address
credentials: Credentials
display_name remains the Python attribute and static type. displayName is
the external Mapping/JSON/output name. Constraints are resolved and normalized
once; contradictory or inapplicable combinations fail at declaration rather
than waiting for a request.
Sensitive redacts Talea-owned repr and validation failures. WriteOnly appears
in standards metadata. Neither marker deletes a successfully validated field
from output automatically, so a service should declare a separate response
contract that never contains credentials.
3. Convert an external Mapping¶
request = AccountCreate.from_mapping(
{
"displayName": "Ada Lovelace",
"address": {
"line_1": "1 Analytical Engine Way",
"city": "London",
"postcode": "SW1A 1AA",
"country": "GB",
},
"credentials": {"token": "correct-horse-battery-staple"},
}
)
assert request.display_name == "Ada Lovelace"
assert isinstance(request.address, Address)
from_mapping() accepts an external Mapping and constructs nested Specs from
nested mappings. Primitive Python values remain strict: an integer field still
does not accept a numeric string. Unknown keys fail instead of being discarded.
This boundary applies the finite default depth, work-node, and error budgets.
Pass a narrower ResourcePolicy when the endpoint's maximum shape is known.
4. Decode JSON representations¶
body = b"""{
"displayName": "Ada Lovelace",
"address": {
"line_1": "1 Analytical Engine Way",
"city": "London",
"postcode": "SW1A 1AA",
"country": "GB"
},
"credentials": {"token": "correct-horse-battery-staple"}
}"""
request = AccountCreate.from_json(body)
from_json() checks encoded size, decodes strict JSON, converts each declared
JSON representation, and validates. The default decoder rejects duplicate
object keys and non-standard NaN/Infinity constants. UUID, Decimal, temporal,
IP, path, and bytes contracts use documented JSON strings because JSON lacks
those Python value types.
Custom codecs can replace only decoding; Talea's compiled conversion still runs afterward. The custom callable is trusted application code and is not sandboxed by ResourcePolicy.
5. Handle realistic failures¶
from talea import ResourceLimitError, ResourcePolicy, ValidationError
try:
request = AccountCreate.from_json(
body,
policy=ResourcePolicy(
max_input_bytes=4_096,
max_depth=8,
max_nodes=200,
max_errors=10,
),
)
except ResourceLimitError as error:
handle_resource_failure(error.code, error.limit, error.observed)
except ValidationError as error:
handle_invalid_request(error.errors(), truncated=error.truncated)
ValidationError.errors() returns fresh JSON-compatible details with stable
codes and nested locations. Consume those facts; do not parse human strings.
Sensitive failure snapshots are redacted. A configured error budget can stop
independent aggregation and set truncated=True.
ResourceLimitError is different: transport size, depth, or node work exceeded
the selected policy. The exception retains only its numeric facts, not the
payload. Your framework decides status codes, logging, retry, and metrics.
6. Return an explicit response¶
from typing import Literal
class AccountResponse(Spec):
account_id: Annotated[int, Alias("id")]
display_name: Annotated[str, Alias("displayName")]
status: Literal["active"] = "active"
response = AccountResponse(
account_id=7,
display_name=request.display_name,
)
response_body = response.to_json()
assert response_body == '{"id":7,"displayName":"Ada Lovelace","status":"active"}'
The response type is an allow-list. It contains no credentials, regardless of
what the request type can serialize. to_dict() produces detached Python
containers; to_json() applies JSON-specific representations and encoding.
Both validate reachable mutable current state before projection.
Use include, exclude, exclude_none, and by_alias for deliberate local
views, but prefer named response Specs when an output shape is a public
contract.
7. Publish input and output schemas¶
input_fragment = AccountCreate.openapi_schema(mode="input")
output_fragment = AccountResponse.openapi_schema(mode="output")
assert input_fragment["schema"]["$ref"].endswith("/AccountCreate")
assert output_fragment["schema"]["$ref"].endswith("/AccountResponse")
The fragments contain a root Schema Object and reachable components. A web framework owns routes, operations, request/response placement, and component merging. JSON Schema projection uses Draft 2020-12 and the same aliases, requiredness, constraints, metadata, recursion, and tags as runtime behavior.
8. Add production composition deliberately¶
From here, add only the features the boundary needs:
- Production service flow: complete raw bytes to response, including resource and validation errors.
- PATCH and presence: absence,
None, defaults, aliases,apply_patch, invariants, and schemas. - Tagged events: direct protocol dispatch, failures, generic envelopes, output, and OpenAPI mappings.
- Contract: containers, TypedDicts, aliases, recursive roots, and generic specializations without wrapper records.
- Errors and security: application handling and hostile-input limits.
- Troubleshooting: broken and corrected examples for common boundary mistakes.
Keep authentication, authorization, database uniqueness, external I/O, transaction behavior, and changing business policy outside structural data contracts. Talea should make a boundary explicit, not absorb the application.