Governed contracts¶
The stringly-typed anti-pattern does not mean every field must be known at compile time. It means that data which can be known must not become an unvalidated string merely for convenience.
A product with custom fields or an administrator-defined metamodel has a different problem: the contract itself is versioned data. MIC calls this a governed contract.
Two families, one level of rigor¶
| Contract | Source of truth | Construction time | Expected validation |
|---|---|---|---|
| Static | Versioned code | Import / compile time | StrictSchema, unknown fields rejected |
| Governed | Versioned registry or database | Runtime | Compiled schema, identified version, unknown fields rejected |
The common rule is simple: no data crosses a boundary without validation against the applicable contract. When the contract was authored does not change the required rigor.
An opaque dictionary that is stored but never interpreted by domain logic is a third valid case. As soon as the service reads a field to decide, filter, or authorize, that field belongs to a static or governed contract.
Required properties¶
A governed contract registry is reliable only if it has all of these properties:
- Versioned source. A logical key is not enough; the version that validated each payload must be identifiable.
- Strict compilation. Types, required fields, bounds, and the unknown-field policy are materialized in a deterministic validator.
- Bounded cache. The cache key includes the contract version. A TTL is a safety net, not the primary consistency mechanism.
- Explicit invalidation. A change publishes an invalidation event or bumps a monotonic version. Instances do not guess that a schema changed.
- Fail closed. A missing, unreadable, or inconsistent schema rejects the
payload instead of falling back to permissive
dict[str, Any]handling. - Observability. Loads, cache hits, invalidations, compilation failures, and the active version are measurable without exposing payload data.
Evolution and compatibility¶
A new version must not silently replace the old one for historical data. The service explicitly chooses a strategy:
- validate each write against the active version and retain that version;
- read historical data using its original version;
- migrate to a new version through an explicit, auditable process.
Incompatible changes receive a new version. Invalidation removes a validator from cache; it does not rewrite previously accepted data.
MIC API¶
SchemaRegistry builds a StrictSchema from a sequence of FieldDef objects
loaded by the application. The generated model remains immutable and rejects
unknown fields. The key, for example resource:k8s:namespace@v3, must carry the
version that the product intends to retain with its data.
from mic.contracts import FieldDef, SchemaRegistry
def load_fields(key: str) -> tuple[FieldDef, ...]:
definition = definitions.get_required(key)
return tuple(
FieldDef(
name=field.python_name,
annotation=field.python_type,
alias=field.external_name,
)
for field in definition.fields
)
registry = SchemaRegistry(
loader=load_fields,
ttl=300,
on_invalidate=metrics.schema_reloaded,
)
Payload = registry.get("resource:k8s:namespace@v3")
payload = Payload.model_validate(raw)
The application still owns the database or registry used as the source of
truth, as well as the invalidation transport. When a change is published, each
instance calls registry.invalidate(key); the TTL bounds divergence if that
event is lost. The loader must reject a missing definition; an inconsistent
definition raises SchemaDefinitionError instead of creating a permissive
model.
SchemaRegistry caches validators inside the process. It does not claim to be
a distributed registry, rewrite historical data, or choose the product's
migration strategy.
See also Strict contracts and Stringly-typed APIs.