Skip to content

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:

  1. Versioned source. A logical key is not enough; the version that validated each payload must be identifiable.
  2. Strict compilation. Types, required fields, bounds, and the unknown-field policy are materialized in a deterministic validator.
  3. Bounded cache. The cache key includes the contract version. A TTL is a safety net, not the primary consistency mechanism.
  4. Explicit invalidation. A change publishes an invalidation event or bumps a monotonic version. Instances do not guess that a schema changed.
  5. Fail closed. A missing, unreadable, or inconsistent schema rejects the payload instead of falling back to permissive dict[str, Any] handling.
  6. 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.