Metadata and sensitive fields¶
Talea has one declaration vocabulary for documentation, boundary classification, and sensitive-data policy. Metadata is immutable, normalized when a declaration is resolved, and retained beside structural validation truth. Introspection, validation failures, serialization failures, and standards projection consume that same record; none re-read annotations.
Field metadata¶
Use Talea markers as top-level Annotated metadata:
from typing import Annotated
from talea import (
Deprecated,
Description,
Examples,
ReadOnly,
Sensitive,
Spec,
Title,
WriteOnly,
)
class Account(Spec):
display_name: Annotated[
str,
Title("Display name"),
Description("Customer-visible account name."),
Examples("Ada", "Grace"),
]
legacy_id: Annotated[int, Deprecated()]
password: Annotated[str, Sensitive(), WriteOnly()]
created_at: Annotated[str, ReadOnly()]
The markers mean:
| Marker | Canonical meaning | Runtime effect |
|---|---|---|
Title(text) |
Human-facing label | None |
Description(text) |
Documentation description | None |
Examples(*values) |
Immutable documentation examples | None |
Deprecated() |
Deprecated classification | No warning |
ReadOnly() |
External-boundary classification | Excluded only by explicit derive_spec(..., mode="input") |
WriteOnly() |
External-boundary classification | Excluded only by explicit derive_spec(..., mode="output") |
Sensitive() |
Error/log safety classification | Redacts failures and repr |
Title, description, examples, and deprecation do not participate in validation. Metadata-free constructors and validators have no metadata loop, registry lookup, or runtime branch.
Examples¶
Examples accept JSON-compatible scalars, finite floats, sequences, and string-keyed mappings. Talea snapshots lists and mappings into recursively immutable canonical values. It does not run transforms, checks, factories, or validation merely to approve documentation examples. Schema projection uses these retained values without executing application code.
Duplicate and unknown metadata¶
One declaration may contain at most one marker of each Talea metadata type.
Duplicates are rejected instead of depending on incidental Annotated order.
An inherited value remains effective when an override does not mention that
metadata type; a local marker replaces the same inherited type.
Unknown third-party Annotated values remain ignored. Talea does not retain
arbitrary objects in canonical declarations or generated runtime artifacts.
Spec metadata¶
Spec-level title, description, examples, and deprecation use one metadata
class keyword:
class Customer(
Spec,
metadata=(
Title("Customer"),
Description("A customer accepted by the account API."),
Examples({"name": "Ada"}),
),
):
name: str
A structured Description wins over the class docstring. Without one, a
non-empty class docstring is captured once as the canonical description.
Talea does not duplicate the Python class name into an explicit title; callers
can use the class identity when title is None.
ReadOnly, WriteOnly, and Sensitive apply to fields and arbitrary
Contracts, not an entire Spec declaration. Spec-level use is rejected because
field paths own these policies.
Dynamic Specs use the same marker sequence:
from talea import create_spec
GeneratedCustomer = create_spec(
"GeneratedCustomer",
{"name": Annotated[str, Description("Display name.")]},
doc="Generated customer contract.",
metadata=(Title("Generated customer"),),
)
There is no dynamic-only metadata representation.
Sensitive failure policy¶
Sensitive() protects Talea-controlled public failure surfaces. For a
sensitive structural, constraint, transform, check, factory, Mapping, JSON, or
Contract failure:
str(exc)renders<redacted>;exc.errors()projects"input": "<redacted>";repr(exc)does not contain the rejected value;exc.valueis<redacted>, not the raw object;exc.received_typestill reports the original concrete type;- Talea does not retain the raw rejected object in the exception;
- a callback or parser cause is dropped instead of attached as
__cause__.
from talea import ValidationError
class Login(Spec):
password: Annotated[str, Sensitive()]
try:
Login(password=123) # type: ignore[arg-type]
except ValidationError as exc:
assert exc.value == "<redacted>"
assert exc.errors()[0]["input"] == "<redacted>"
This deliberately refines the ordinary Error Experience API. A non-sensitive
failure still retains the exact rejected object through exc.value and keeps
documented callback causes for debugging. Sensitive failures have no implicit
unsafe/debug escape hatch: submit the relevant non-secret state separately if
an incident needs more evidence.
Nested paths and containers¶
Sensitivity follows canonical declaration structure through nested Specs, lists, mappings, sets, unions, recursive Specs, TypedDict fields, generic specializations, and Contract input. Static field names and list indexes remain useful locations. Value-derived mapping keys and set members are themselves redacted when they are beneath a sensitive declaration.
An enclosing sensitive field protects a nested failure even when the nested
field is not independently marked. A nested sensitive declaration remains
protected when an outer declaration adds a prefix. Talea never infers secrecy
from names such as password, token, or secret.
Aliases change the external location name, not the security policy:
from talea import Alias
class TokenRequest(Spec):
token: Annotated[str, Alias("access-token"), Sensitive()]
from_mapping and from_json report access-token while redacting its value.
Transforms, checks, and causes¶
Talea-controlled rendering never includes callback exception text. For an ordinary field the cause remains attached; for a sensitive field Talea drops it because a callback can embed the secret in its message or attributes. A whole-Spec check is sensitive when any declared target is sensitive.
Talea cannot stop application callbacks from logging, transmitting, or otherwise exposing their arguments before raising. Sensitive metadata governs Talea's failure objects; callback code remains trusted application code.
Serialization¶
Sensitive and write-only are deliberately different:
Sensitiveprotects errors and representation used for diagnostics.WriteOnlyclassifies an external output policy for schemas and adapters.
Normal source-Spec to_dict() and to_json() include sensitive and write-only
fields. Metadata does not silently change the round-trip contract:
credentials = Login(password="correct horse")
assert credentials.to_dict() == {"password": "correct horse"}
assert "correct horse" not in repr(credentials)
If a serialization hook under a sensitive field fails, its
SerializationError keeps the field location but drops the callback cause.
Likewise, a JSON codec failure for a value graph containing sensitive metadata
does not retain the codec exception. Successful custom hooks and codecs still
receive the actual value because serialization was explicitly requested.
A Sensitive represented value follows the same policy for current-state, dumper, invalid-result, nested projection, and JSON failures. Talea suppresses unsafe causes and never renders callback repr or message-derived details. It cannot stop the trusted loader/dumper from logging its argument or causing external side effects, and successful represented output remains intentional output rather than automatic omission.
Read-only and write-only are also projected to JSON Schema/OpenAPI. They do not enforce ordinary source-Spec runtime operations. Applications that want a concrete directional shape can explicitly derive it:
from talea import derive_spec
LoginInput = derive_spec(Login, mode="input")
LoginOutput = derive_spec(Login, mode="output")
The input view structurally lacks effective read-only fields, and the output
view structurally lacks effective write-only fields. Retained metadata stays on
retained fields. A field marked both ways is absent from both views; an explicit
false marker clears inherited classification. This selection reads normalized
SpecField.metadata, not the original annotation.
Inheritance and explicit opt-out¶
Metadata follows field override ownership:
class BaseCredential(Spec):
value: Annotated[int | str, Sensitive(), Description("Credential value.")]
class NumericCredential(BaseCredential):
value: int
NumericCredential.value remains sensitive and keeps its description. This
prevents an ordinary narrowing override from silently widening information
exposure. Sensitive(False) is the explicit field-level opt-out:
Deprecated(False), ReadOnly(False), and WriteOnly(False) provide the same
explicit override form. Removing sensitivity from an alias's own identity is
not a use-site operation; declare a different alias if its security identity is
different.
Type aliases and TypedDict¶
Metadata inside a PEP 695 alias belongs to the named alias identity. Metadata outside the alias belongs to the field or Contract use site:
type SecretId = Annotated[int, Description("Alias identity."), Sensitive()]
request_id = Contract(Annotated[SecretId, Title("Request identifier")])
The Contract's title is the use-site title; the alias description remains its
fallback identity documentation. Alias sensitivity cannot be downgraded by a
use-site Sensitive(False) marker.
TypedDict child annotations retain their own metadata:
from typing import TypedDict
class Credentials(TypedDict):
password: Annotated[str, Sensitive()]
contract = Contract(Credentials)
Strict validation, external Python conversion, and JSON input all redact a
failure at password. Python's typing.ReadOnly qualifier remains separate
TypedDict structural truth; Talea's ReadOnly() marker is boundary metadata.
Introspection¶
FieldInfo, SpecInfo, and ContractInfo expose normalized title,
description, examples, deprecation, read/write, and sensitivity values where
they apply. The descriptions are frozen projections. Mutating returned example
containers or introspection dataclasses cannot change canonical declarations.
Open generic introspection exposes metadata before the field schema can be resolved. Concrete specialization and recursive finalization retain the same metadata without copying it per instance or per recursive expansion.
JSON Schema and OpenAPI projection¶
json_schema() and openapi_schema() project titles, descriptions, examples,
deprecation, readOnly, and writeOnly from canonical field, Spec, Contract,
TypedDict, and alias metadata. Sensitive is intentionally absent from public
standards output because it is a Talea error-redaction policy, not a standard
schema keyword. See JSON Schema and OpenAPI.
Security guidance and limitations¶
- Mark the canonical declaration; do not rely on field-name heuristics.
- Treat successful serialization as intentional secret access.
- Do not put secrets in titles, descriptions, examples, aliases, constraint boundaries, or callback names. Those values are declaration truth, not rejected input.
- Application callbacks can leak their own arguments through logs or external effects; Talea only controls its own errors.
- Sensitive error objects discard raw input and causes. Capture separate safe diagnostic facts before crossing the validation boundary when required.
- Read-only and write-only are direction classifications. Explicit derived views enforce their field shape, but they do not provide authentication, authorization, immutability, persistence protection, or secret handling.
Metadata is class/Contract-owned cold state. Instances retain only field values, and metadata-free successful validation and serialization have no metadata traversal.