Runtime Form Validation with Backend JSON Schemas: A Practical Architecture
How to keep runtime validation, type safety, and manual UX aligned when JSON Schemas come from backend contracts
If your backend owns JSON Schemas and your frontend receives them over HTTP, you can still get fast, safe, maintainable form validation without turning your UI into a schema renderer.
That setup is often the sweet spot:
- Backend owns contracts
- Frontend owns UX
- JSON Schema drives validation and capability hints
- UI stays intentionally manual
The Core Problem
Most teams want all of this at once:
- Runtime validation
- Type safety
- Dynamic behavior
- Shared backend/frontend contracts
- Good error messages
- Conditional rules that stay understandable
The failure mode is predictable:
- TypeScript types drift from runtime schemas
- Validation logic gets duplicated across frontend and backend
- Conditional logic becomes unreadable
Single Source of Truth (Without Surrendering UX)
Use JSON Schema as the contract source of truth.
Then build a frontend pipeline that interprets schema capabilities, not full UI structure:
Backend JSON Schema
↓
Ajv compile/cache
↓
Schema adapter/model
↓
Field capabilities (required, readOnly, enum, constraints, visibility)
↓
Manual React UI
This avoids the classic schema-driven-form trap where layout, flow, and product behavior become accidental.
Recommended Stack
For TypeScript/JavaScript:
- Runtime validation: Ajv
- Schema authoring/generation (when relevant): TypeBox or Zod +
zod-to-json-schema - Form integration (manual UI): React Hook Form
- Schema-driven UI (only if needed): JSON Forms or react-jsonschema-form
In this architecture, JSON Schema drives validation and capabilities, not full rendering.
Why Ajv Is the Runtime Workhorse
Ajv is usually the right runtime choice because it:
- Compiles schemas to JavaScript validators
- Performs extremely well at scale
- Supports caching and standalone generated validators
Minimal flow:
import Ajv from 'ajv';
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schemaFromBackend);
const isValid = validate(formData);
if (!isValid) {
console.log(validate.errors);
}
Important rule: compile once and cache by schema ID/hash. Never recompile on every render.
Add a Schema Adapter Layer
The biggest maintainability win is a small adapter that hides raw JSON Schema internals from components.
Expose semantic metadata instead:
type FieldMeta = {
required: boolean;
readOnly: boolean;
visible: boolean;
enum?: string[];
min?: number;
max?: number;
};
Instead of repeating checks like schema.properties?.email?.readOnly everywhere, UI code consumes fieldMeta.
This prevents:
- Schema lookup duplication
- Tight coupling to schema syntax (
allOf,if/then/else,$ref) - Version-fragile component logic
Validation Timing Matters as Much as Validation Rules
JSON Schema answers: is this object valid?
UI needs: when and how should we validate for a good experience?
A robust flow:
- Lightweight field-level checks during editing
- Incremental validation for changed fields
- Full schema validation on submit/save
- Separate async pipeline for server checks (uniqueness, permissions, workflow constraints)
Avoid:
- Full-form validation on every keystroke
- Embedding business workflow logic inside schema conditionals
Conditional Logic: Keep Structure and Business Rules Separate
JSON Schema is great for structural constraints:
- Shape
- Required fields
- Enum values
- Formats
- Numeric/string limits
It becomes harder to maintain when used for complex domain workflows.
Use this split:
- Schema layer: structure and static constraints
- Business rule layer: pricing logic, permissions, cross-field process rules, async domain checks
Common Sharp Edges (and Fixes)
1) Required but hidden fields
This often appears with conditional schemas.
Fix with one of:
- Schema-aware visibility derivation
- Hidden field default injection
- Conditional data pruning before submit
2) Schema drift across services
Fix by generating validators/types/tooling from the contract source and versioning schema IDs.
3) Monolithic schemas
Fix by composing smaller domain schemas and keeping field modules bounded.
Scenario-Based Recommendations
API-first platform
- JSON Schema + Ajv + TypeBox/OpenAPI alignment
React product app
- Zod + React Hook Form for primary DX
- Export JSON Schema only where contract interoperability is needed
Dynamic admin/CMS builder
- JSON Schema + Ajv + JSON Forms
Large enterprise system
- Canonical JSON Schema contracts
- Validator precompilation
- Schema registry + versioning
Takeaway
If backend teams own JSON Schemas, your frontend can still stay elegant:
- Treat schema as contract + capability metadata
- Compile and cache validators once
- Isolate schema interpretation in an adapter/model layer
- Keep UI manually designed and product-driven
- Split structural validation from business workflow logic
That gives you runtime safety, scale, and maintainability—without sacrificing UX intent.
#typescript #jsonschema #react #architecture #validation