Skip to main content

Field declarations

A schema contract is normally inferred from data you have already sent. Field declarations let you state your fields up front instead: each field's type, its privacy class, the role you propose for it, how often you expect it to be missing and a description. Tessera puts the declared fields into a contract draft, so you can review and approve them before the first event arrives. Approval itself does not change: nothing reaches a model until a project owner or admin approves the contract.

Declarations belong to the project, like its contract: the live and test environments read the same document.

The declarations document​

PUT /v1/field-declarations replaces the project's declarations with one document. Each declaration names an (event_type, attr_key) pair once:

{
"declarations": [
{
"event_type": "ticket.resolved",
"attr_key": "resolution_minutes",
"declared_type": "number",
"pii": "none",
"role": "feature",
"missingness": "rare",
"description": "Minutes from opening to resolution.",
"annotations": { "unit": "minute" }
},
{
"event_type": "ticket.resolved",
"attr_key": "requester_email",
"declared_type": "categorical",
"pii": "direct",
"role": "excluded"
}
]
}
MemberRequiredValues
declared_typeyesnumber, integer, categorical, boolean, timestamp, text, array
piiyesnone, direct, quasi
rolenofeature, target_only, identifier, metadata, excluded (a proposal: approval decides)
missingnessnoexpected, rare, never
descriptionno1 to 4,096 characters
annotationsnoup to 32 keys (lower_snake_case) with string values of up to 1,024 characters

These are the same value sets the contract uses for its attributes. A member you omit is inferred, as it would be without a declaration.

Annotations are yours. Tessera stores them and returns them exactly as you sent them, and never acts on them. Use them for anything else you track about a field, such as a unit, a lifecycle stage or a retention rule.

A document holds at most 4,096 declarations and 4 MiB of canonical JSON.

What a put does​

  1. If the document is identical to the current version, Tessera answers that version and writes nothing, so retrying a put is safe. Pass expected_version (the version you last read, or 0 for none) to refuse a put when someone else changed the declarations first.

  2. Tessera stores the next version. Versions are immutable; read the current one with GET /v1/field-declarations and any older one with GET /v1/field-declarations/{version}.

  3. Every declared field enters the project's profiling input, so contract drafts include it before any event does.

  4. Tessera creates one contract draft from the declaration and returns its version as contract_version:

    • a field the approved contract does not hold yet takes your declared type (with its default encoding), role, privacy class, missingness and description instead of the inferred ones;
    • a field the approved contract already holds takes your role, privacy class, missingness and description, exactly as an edit of the draft would. Its type never changes once approved.

    When the draft would equal the approved contract, no draft is created and contract_version is null.

  5. Review the draft with GET /v1/contracts/{version} and its differences with GET /v1/contracts/{version}/diff, then approve it with POST /v1/contracts/{version}:approve. The put never approves.

Later drafts (POST /v1/contracts, or a domain pack install) keep the decisions of the approved contract and use your declarations, instead of inference, only for fields the approved contract does not hold yet.

Removing a declaration​

Removing a field from your declarations does not remove it from the project. A declared field stays in the project's profiling input, so a later draft still lists it, now with inferred values. If it should not reach a model, keep it out explicitly: declare it with the role excluded or metadata, or set that role in the draft (PATCH /v1/contracts/{version}) before you approve it. A field the approved contract already holds keeps its approved values either way.

Refusals​

A put that would contradict the approved contract or the draft rules is refused as a whole with failed_precondition, and nothing is stored. Each refused declaration is listed in error.details with its reason:

reasonMeaning
declared_type_conflictThe approved contract holds the field with another type; an approved type never changes.
pii_direct_featureA direct privacy class cannot be combined with the feature role.
identifier_as_featureThe field is an identifier (by its name or its values) and cannot be a feature.

A malformed document (an unknown member, a duplicate field, a value outside the bounds above) is refused with invalid_argument.

Access​

Reading declarations needs the same access as reading contracts: the viewer role, or an API key with the read scope. Replacing them needs the admin role (or owner), or an API key with the admin scope.