Skip to content

Software Quality

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 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: 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), 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 and the recurring parser and binary-reader bugs that fuzzing 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, which tracks untrusted data along the path from source to sink, and of fuzzing the boundary with hostile corpora such as the 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 (Python) — validates data against annotated models; the runtime enforcement of Python type hints.
  • zod (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 — the interchange format for declaring and validating JSON structure across languages.
  • OpenAPI — 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. Validation checks shape, not permission.
  • As a substitute for a contract 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) or the schema-valid-but-hostile layer above it (taint analysis, fuzzing).

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.

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: 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