# Schema and boundary validation

Schema validation checks that data arriving from outside a program satisfies a
declared shape and set of constraints before the program acts on it. The
**schema supplies the oracle**: a machine-readable specification (field names,
types, ranges, required-ness, formats) that the boundary accepts or rejects data
against. A request body, a config file, a queue message, or a third-party API
response is parsed against the schema; conforming data passes through as a typed
value, and anything else is rejected at the edge.

## The border crossing: from untyped to typed

Validation turns untyped external data (JSON text, form fields, environment
strings, wire bytes) into a typed internal value, and how much of that crossing
the type system forces depends on its soundness. A
[sound](https://quality.stereobooster.com/types.md#soundness-the-central-property) system forbids
fabricating a typed value from data it never checked, so the *shape* of the value
cannot be faked: a checked
conversion is the only way across. An unsound system (TypeScript, mypy, Java at
its boundaries) forces nothing, and unchecked external data can masquerade as a
type nothing ever verified. Validating the shape anyway is what makes the
internal type trustworthy rather than a claim.

The crossing shares one thing with a
[contract](https://quality.stereobooster.com/contracts-and-runtime-assertions.md): once the value is parsed,
downstream code treats it as trusted, the way it trusts a value that has
satisfied a precondition. What inverts is the direction of trust. A contract
trusts its caller and reads a violation as a bug in that caller; a boundary
validator distrusts its input and reads a violation as expected input to reject.
That inversion is what makes this validation and not a contract, and it is why
a validator belongs at the untrusted edge while a contract guards an internal
boundary between trusted code.

Checking at the crossing also moves the failure to its source. A shape mismatch
surfaces at the boundary with the offending field named, instead of down the line
as a confusing failure deep inside a handler that assumed a value it never had.
When the schema also derives the internal type ([static
types](https://quality.stereobooster.com/static-types.md)), one declaration serves both the runtime check
and the static type behind it.

## When the caller is a user

When the untrusted source is a person filling in a form, a violation is neither
a bug nor an attack. It is expected, and the right response is a message
returned to the sender, not a crash, a log line, or a developer alert.

The same violation has a different disposition depending on who is on the other
side:

- **A contract** on an internal boundary: the caller is trusted code, so a
  violation is a bug and the program crashes or alerts its developers.
- **An API-client boundary**: the caller is another program, so a violation is
  answered with a machine-readable error (an HTTP 4xx and an error body) for that
  program to handle.
- **A user-facing form**: the caller is a person, so a violation is answered with
  a human message, typically field-level, translated, and accumulated so every
  problem is reported at once rather than failing on the first.

One schema can drive all three; only the error channel and its presentation
differ. Libraries reflect this: Zod and Pydantic expose
structured error objects that a UI layer formats for a person, an API layer
serializes for a caller, or a service treats as a fatal contract breach.

## Security is layered, not one wall

A boundary's security is not a single check but a stack of layers, and schema
validation owns only the middle one. The layers are a model, not clean phases: a
streaming parser validates structure as the bytes arrive, so the parse and the
schema check interleave rather than running strictly one after the other.

- **Bytes to structure.** Turning raw bytes into a structure is where buffer
  overflow, memory corruption, and type confusion live, and where a decompression
  bomb, a deeply nested document that overflows a recursive parser, or an
  oversized length prefix exhausts memory before any complete structure exists.
  These sit *below* schema validation and are closed by memory-safe parsing and
  hardened readers that bound the input as bytes become values, not by a schema:
  see [memory safety](https://quality.stereobooster.com/memory.md#safety-violations) and the
  recurring parser and binary-reader bugs that [fuzzing](https://quality.stereobooster.com/fuzzing.md)
  targets. A schema running on an already-parsed structure never reaches this
  layer, which is why production JSON and XML parsers carry their own depth and
  size limits.
- **Structure to schema.** Given a well-formed structure, reject the shapes the
  schema forbids: missing required fields, wrong types, values outside declared
  ranges, and payloads past a size or depth cap (a backstop to the parser's own
  limits, not a replacement for them). This is the layer this method owns.
- **Schema-valid but hostile.** A payload can satisfy the schema and still be an
  attack: an injection string in a free-text field, or a well-typed value that is
  dangerous once it reaches a downstream sink. Schema shape does not catch these.
  They are the province of [taint analysis](https://quality.stereobooster.com/taint-analysis.md), which
  tracks untrusted data along the path from source to sink, and of
  [fuzzing](https://quality.stereobooster.com/fuzzing.md) the boundary with hostile corpora such as the
  [Big List of Naughty Strings](https://github.com/minimaxir/big-list-of-naughty-strings). Rejecting unexpected extra
  fields (a strict schema) sits on the line between this layer and the one above.

A validator defines the intended boundary and a fuzzer tests it: the schema
states where the boundary is, and fuzzing searches for inputs it accepts but
should not, or inputs that crash the parser itself. Separate from every layer is
authorization: a well-formed, well-typed, non-hostile request can still be one
the sender is not permitted to make, and shape says nothing about permission.

## The schema as one artifact

A schema is a single declaration that many tools read in different ways. Its
verification roles are the runtime validation predicate and the
structure-to-schema security check. From that same declaration come uses that
verify nothing:

- **Serialization and deserialization.** Schema-defined wire formats such as
  Protocol Buffers and Avro govern how a value is encoded and how versions stay
  compatible.
- **Client and server generation.** An OpenAPI document generates a typed API
  client for callers and request-routing and validation stubs for the server.
- **Mock-server generation.** A schema generates a fake server that returns
  schema-valid responses, so a consumer can be built and tested before the real
  provider exists.
- **Documentation.** The same document renders the human-readable API reference.

## What it catches

- **Malformed structure.** Missing required fields, wrong types, extra
  unexpected keys, values outside declared ranges: rejected before any handler
  runs.
- **Boundary type confusion.** A number arriving as a string, a date in the
  wrong format, an enum value the code does not handle.
- **Oversized and over-nested payloads.** Size and depth caps at the schema layer,
  backing the parser's own limits on the input below.
- **Interface drift.** A caller sending a shape the current schema no longer
  accepts, or an upstream API returning a shape the consumer no longer expects.

What it does not catch: anything the schema does not state, anything at the
byte-to-structure layer below it, and anything hostile at the sink above it. A
validator confirms the data has the declared shape, not that the shape is the
right one or that a well-formed request expresses a legitimate operation.

## Tools

### Validation libraries

- **[Pydantic](https://docs.pydantic.dev/)** (Python) — validates data against annotated
  models; the runtime enforcement of Python type hints.
- **[zod](https://zod.dev/)** (TypeScript) — schema-first validation that derives a
  static type from the schema.
- **JSR-303 / Jakarta Bean Validation** (Java), **serde** with **validator** (Rust) — 
  the same shape-and-constraint check in their ecosystems.

### Schema and interface specifications

- **[JSON Schema](https://json-schema.org/)** — the interchange format for declaring and
  validating JSON structure across languages.
- **[OpenAPI](https://www.openapis.org/)** — describes HTTP APIs; the schema drives request
  and response validation, generated clients, server stubs, mock servers, and
  documentation from one document.
- **Protocol Buffers** and **Avro** — schema-defined wire formats where the
  schema governs serialization and compatibility.

## When to use, when not

**Use:**

- At every boundary where external data enters — request bodies, config,
  webhooks, queue messages, third-party responses.
- On user-facing input, where a violation is an expected outcome to report back
  as a readable, field-level message, not an internal fault to fail fast on.
- Where the interface is shared across services and should be described once and
  enforced on both sides. An OpenAPI or JSON Schema document serves the check and
  the documentation together.
- Where the parsed result should carry a static type inside the program, using a
  tool that derives the type from the schema.

**Don't:**

- As a substitute for [authorization](https://quality.stereobooster.com/security.md).
  Validation checks shape, not permission.
- As a substitute for a [contract](https://quality.stereobooster.com/contracts-and-runtime-assertions.md) on an
  internal boundary, where the caller is trusted code.
- As the whole of boundary defense. It does not cover the byte-to-structure layer
  below it ([memory safety](https://quality.stereobooster.com/memory.md#safety-violations)) or the
  schema-valid-but-hostile layer above it
  ([taint analysis](https://quality.stereobooster.com/taint-analysis.md),
  [fuzzing](https://quality.stereobooster.com/fuzzing.md)).

## Evidence

No controlled study isolates schema and boundary validation's effect on defects
or incidents. Its warrant is definitional and narrow: a validator rejects the
malformed and out-of-range structure its schema names, at the edge, before
internal code trusts the value. That is an empirical property bounded by how
complete the schema is, not an all-inputs guarantee.

## Related

**Looks alike — Design by Contract vs schema validation**

Mechanically identical — evaluate a predicate at runtime and throw — so the
mechanism doesn't tell them apart; what's being checked does. Validating *untrusted*
boundary data (an HTTP body, an env var) is
**validation**: a failure is
expected and you handle it (a `400`). Asserting *trusted* internal state
(`balance >= 0`, a returned list is sorted) is a [**contract**](https://quality.stereobooster.com/contracts-and-runtime-assertions.md):
a failure is a bug, so it crashes and alerts. The same `z.refine(...)` is
validation on a request body and a contract on an internal invariant.

## Classification

- **Quality dimensions:** Functionality, Security (rejects malformed and out-of-bounds structure at the untrusted boundary before internal code trusts it), Maintainability (one schema is the single source of the internal type and of the generated client, server, and mock; a schema change breaks mismatched consumers at type-check time).
- **Area:** HTTP and RPC request bodies, config and environment, message-queue payloads, file and form uploads, third-party webhook and API responses — any data crossing from an untrusted external source into a program.
- **Guarantee:** Empirical — a violation is rejected when it arrives; a clean parse is evidence the input conformed, not proof the schema is complete.

## Referenced by

- [Contracts and runtime assertions](https://quality.stereobooster.com/contracts-and-runtime-assertions.md) · Methods
- [Static types](https://quality.stereobooster.com/static-types.md) · Methods
- [Types and effects](https://quality.stereobooster.com/types.md) · Methods
- [TypeScript](https://quality.stereobooster.com/typescript.md) · Recipes
- [Choosing methods](https://quality.stereobooster.com/choosing.md) · Overview
