illuminate/json-schema
Official Laravel JSON Schema component for defining and validating structured JSON data with a fluent, framework-native API. Part of the Illuminate suite, it offers a clean, expressive way to describe schemas and ensure payloads conform, making it ideal for APIs, configuration, and more in Laravel a...
illuminate/json-schema ships inside laravel/framework (namespace Illuminate\JsonSchema), so a Laravel 11/12 app already has it — no install step. For a standalone/non-Laravel context, composer require illuminate/json-schema and bind Illuminate\JsonSchema\JsonSchemaFactory yourself.
Read these first, in order:
src/Illuminate/JsonSchema/JsonSchemaFactory.php — the entry point behind the JsonSchema facade (Illuminate\Support\Facades\JsonSchema, accessor json-schema). It exposes the type constructors: string(), integer(), number(), boolean(), array(), object(), enum(), anyOf()/union(), any().src/Illuminate/JsonSchema/Types/Type.php — the shared base: title(), description(), default(), examples(), enum(), const(), nullable(), and toArray().Types/ObjectType.php, ArrayType.php, StringType.php, NumberType.php — the type-specific constraint methods (properties(), required(), items(), minLength(), pattern(), minimum(), …).The package is a schema builder, not a validator: it produces a JSON Schema (draft 2020-12 shaped) array via ->toArray(). Your first useful thing is a schema for a payload you already validate by hand:
use Illuminate\Support\Facades\JsonSchema;
$schema = JsonSchema::object()
->properties([
'name' => JsonSchema::string()->minLength(1)->maxLength(120),
'email' => JsonSchema::string()->format('email'),
'age' => JsonSchema::integer()->minimum(0),
'tags' => JsonSchema::array()->items(JsonSchema::string())->uniqueItems(),
])
->required(['name', 'email'])
->additionalProperties(false)
->toArray();
// hand to OpenAPI, an LLM tool definition, opis/json-schema, etc.
Wrap schemas in small, named builder classes. Don't scatter fluent chains through controllers. A final class CreateUserSchema { public static function make(): Type { ... } } gives you one place to version and test. Snapshot ->toArray() in a test so schema drift is visible in diffs.
Reuse sub-schemas rather than repeating them. Build leaf types once (an AddressSchema, a MoneySchema) and splice them into parents via properties([...]), items(...), or anyOf([...]). Because types are plain objects, this composes cleanly and keeps required() lists close to the properties they describe.
LLM tool / structured-output definitions. This is the highest-leverage day-to-day use: the fluent API maps almost 1:1 onto function-calling JSON Schema. Emit ->toArray() as the tool's parameters, keep description() on every property (models use it), and set additionalProperties(false) aggressively so the model can't invent keys. Pair with a validation pass on the model's response before trusting it.
OpenAPI / API contract generation. Generate request and response schemas from the same builders used by your FormRequests, then feed them into your OpenAPI generator. This keeps docs and runtime expectations from diverging — the classic failure mode is hand-maintained YAML drifting from real validation.
Validation pipeline. Since the package stops at schema generation, wire toArray() into Opis\JsonSchema\Validator (or justinrainbow/json-schema) in a dedicated JsonSchemaValidator service, and cache the compiled schema object per request/schema class. A typical flow: build schema → validate raw array → hand the clean array to a DTO/Data::from(). Keep this separate from Laravel's Validator; mixing rule-based and schema-based validation in one class makes failures hard to attribute.
Schema-driven UI and form generation. Emit the same schema to the frontend and render fields from it; enum(), default(), format(), and required give you enough metadata for basic form rendering without a second source of truth.
Config and payload guards. For webhook receivers or queue payloads with a published contract, define the schema in code, validate on ingest, and log structured failures (opis gives you pointer paths like /user/email) instead of generic "invalid payload" errors.
required lives on the object, not the property. In JSON Schema, required is an object-level keyword. Use ->required(['a','b']) on ObjectType. If you see a per-property ->required() in examples, verify against Types/ObjectType.php in your installed version — semantics and the exact chaining style have shifted across framework releases.additionalProperties defaults to permissive. Omitting it means "anything goes," which silently defeats the point of a schema. Set false explicitly on every object you care about, especially LLM tool schemas.format is annotation-only for many validators. ->format('email') documents intent; opis and most engines won't enforce it unless you opt into format assertion. Enforce emails with Laravel's email rule or a pattern().integer vs number matters. JsonSchema::integer() rejects 1.0 in strict validators; number() accepts both. Also remember PHP's json_encode will turn a float 1.0 into 1, which flips validation outcomes.properties must be an object. An empty PHP array encodes as [], not {}. If you build schemas dynamically and can end up empty, cast ((object) []) or ensure at least one property; otherwise validators reject the schema itself.->nullable() (or a union with JsonSchema::null()) is required; an optional property is absent, not null. This trips people coming from Laravel validation's nullable.Illuminate\Validation\Rules\Json. That rule validates that a string is JSON; this package describes the shape of decoded data. They solve different problems.JsonSchemaFactory from the container (or inject JsonSchemaFactory) instead of calling the facade statically if you need to swap or extend type construction; the facade is a thin singleton and hard to fake otherwise.Types\Type (and the relevant concrete type) if you need custom keywords, but check the factory for a registration hook first — the factory is the intended seam, and custom types can be lost if you resolve the factory multiple times without binding it as a singleton.dd($schema->toArray()) and paste into any JSON Schema validator playground. Nearly every "my schema doesn't validate" bug is a nesting error (properties placed under the wrong object) or a missing required, not a package bug.laravel/framework; the fluent surface is young and has changed. Pin your framework minor if you snapshot schemas in tests, and re-run those snapshots on upgrade.How can I help you explore Laravel packages today?