rushing / laravel-data-schemas
Generate JSON Schemas from Laravel Data objects with validation attributes
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- opis/json-schema: ^2.3
- rushing/laravel-popcorn: dev-main
- spatie/laravel-data: ^4.0|^5.0
- symfony/finder: ^6.0|^7.0|^8.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- v1.1.1
- v1.1.0
- dev-main / 1.0.x-dev
- v1.0.2
- v1.0.1
- v1.0.0
- v0.1.0
- dev-slice/owner-review-b27
- dev-candidate/morph-union-runtime-discriminator-check
- dev-slice/ep-02-collect-morph-union-base
- dev-slice/docs-truth-20261016
- dev-slice/ep-02-morph-union
- dev-candidate/schemas-fresh-boot-cache
- dev-candidate/data-config-fail-loud
- dev-candidate/combined-app07-camel-v3
This package is auto-updated.
Last update: 2026-11-04 20:35:25 UTC
README
Generate JSON Schemas from Laravel Data objects with validation attributes.
This package automatically converts your Spatie Laravel Data objects into JSON Schema files, preserving validation rules, descriptions, and examples. Perfect for AI-powered applications, API documentation, and frontend validation.
Features
- 🎯 Single Source of Truth: Define structure, validation, types, and schemas all in your Data objects
- 🔄 Automatic Generation: Run
schemas:generatesimilar totypescript:transform - 📝 Rich Metadata: Add descriptions and examples via custom attributes
- 🎨 Flexible Output: Configure path structure (namespace, flat, or custom)
- 🔌 Extensible: Custom collectors, generators, and path generators
- ✅ Validation Mapping: Automatic conversion of Spatie validation attributes to JSON Schema constraints
Installation
Install via Composer:
composer require schemastud/laravel-data-schemas
Publish the configuration file (optional):
php artisan vendor:publish --tag=data-schemas-config
Usage
1. Add Attributes to Your Data Objects
<?php namespace App\Data; use Spatie\LaravelData\Data; use Spatie\LaravelData\Attributes\Validation\Max; use Spatie\LaravelData\Attributes\Validation\Email; use Schemastud\DataSchemas\Attributes\Description; use Schemastud\DataSchemas\Attributes\Example; class UserData extends Data { public function __construct( #[Description('User full name')] #[Example('John Doe')] #[Max(255)] public string $name, #[Description('User email address')] #[Example('john@example.com')] #[Email] public string $email, #[Description('User age in years')] #[Example(25)] #[Example(42)] public ?int $age = null, ) {} }
2. Generate JSON Schemas
# Generate all schemas php artisan schemas:generate # Generate for specific path php artisan schemas:generate --path=app/Data/Schemas # Generate for specific class php artisan schemas:generate --class="App\Data\UserData" # Override output directory php artisan schemas:generate --output=storage/schemas
3. Use Generated Schemas
// resources/schemas/App/Data/UserData.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "UserData", "type": "object", "title": "UserData", "properties": { "name": { "type": "string", "description": "User full name", "examples": ["John Doe"], "maxLength": 255 }, "email": { "type": "string", "description": "User email address", "examples": ["john@example.com"], "format": "email" }, "age": { "type": ["integer", "null"], "default": null, "description": "User age in years", "examples": [25, 42] } }, "required": ["name", "email", "age"] }
Choose the request or response projection
For an editable form, resolve the host's configured generator chain and select request mode. For a record's read projection, select response mode:
use App\Data\UserData; use Schemastud\DataSchemas\Generators\Generator; $generator = app(Generator::class); $requestSchema = $generator->forRequest()->generate(new ReflectionClass(UserData::class)); $responseSchema = $generator->forResponse()->generate(new ReflectionClass(UserData::class));
The container binding preserves data-schemas.generators and the host's strategies/configuration;
constructing a bare generator bypasses that configured chain. These mode methods return variants,
so selecting one does not change the other. Request mode follows input-mapped names and omits
#[Computed] properties; response mode follows output-mapped names and includes computed output.
Hidden properties remain absent in both.
Nullability and omission are independent. ?string $name without a default is required but accepts
null. A constructor default such as ?int $age = null makes the field omittable in request mode;
response mode still requires it because the Data object serializes its value. Optional and Lazy
types are not required in either ordinary projection. The default collapsed mode retains the
response required/default rule, so the example above includes age in required; it is not the
schema to reuse unchanged for editable input. A JSON Schema default annotation alone does not
fill values or validate a submitted payload. Use the declared Data input and server validation.
Configuration
The config/data-schemas.php file provides extensive configuration options:
return [ // Paths to scan for Data objects 'auto_discover_types' => [ app_path('Data'), ], // Filter by namespace patterns (optional) 'namespaces' => [ // 'App\\Data\\Schemas\\*', ], // Output directory 'output_directory' => resource_path('schemas'), // Path structure: 'namespace', 'flat', or 'custom' 'path_structure' => 'namespace', // Custom path generator callable 'custom_path_generator' => null, // Pretty print JSON 'format_output' => true, // JSON Schema version 'schema_version' => 'https://json-schema.org/draft/2020-12/schema', // Include $schema and $id 'schema_metadata' => [ '$schema' => true, '$id' => true, ], // Custom validation attribute mappings 'validation_mapping' => [], ];
Path Structure Options
Namespace Structure (default)
resources/schemas/
└── App/
└── Data/
└── UserData.schema.json
Flat Structure
resources/schemas/
├── UserData.schema.json
├── ProductData.schema.json
└── OrderData.schema.json
Custom Structure
'path_structure' => 'custom', 'custom_path_generator' => function (ReflectionClass $class, string $baseDir) { return $baseDir . '/api/' . $class->getShortName() . '.json'; },
Supported Validation Attributes
The package automatically maps Spatie validation attributes to JSON Schema constraints:
| Validation Attribute | JSON Schema Constraint |
|---|---|
#[Max(255)] |
maxLength (string), maximum (number), maxItems (array) |
#[Min(1)] |
minLength (string), minimum (number), minItems (array) |
#[Between(1, 100)] |
minimum + maximum |
#[Email] |
format: "email" |
#[Uuid] |
format: "uuid" |
#[Url] |
format: "uri" |
#[Enum(StatusEnum::class)] |
enum: [values] |
#[Nullable] |
type: ["string", "null"] |
Registry values
Declare a live registry constraint on a Data property to use the same membership rule for Spatie validation and generated JSON Schema:
use Schemastud\DataSchemas\Attributes\ExistsInRegistry; use Spatie\LaravelData\Data; class ExportData extends Data { public function __construct( #[ExistsInRegistry('app.formats', relative: true)] public string $format, ) {} }
For registrations app.formats.json and app.formats.markdown, validation accepts json and
markdown, and the schema publishes those strings as its enum. Omit relative: true to use fully
qualified keys. A domain attribute can extend ExistsInRegistry and override constraint() to
return a Popcorn rule with a subset predicate; both projections use that rule.
Enumeration honors current registry visibility and is recomputed during generation. Publish under
the intended documentation audience; a saved enum is a snapshot, not an authorization policy.
An empty vocabulary emits not: {} (no valid value), while a nullable field still admits null.
Registry rules returned only by a Data class's rules() method continue to validate, but require the
attribute declaration above to project their enum into schemas.
Custom Attributes
Description
Add human-readable descriptions to properties or classes:
#[Description('User profile information')] class UserData extends Data { #[Description('Primary email address for notifications')] public string $email; }
Example
Add one or multiple examples (repeatable attribute):
#[Example('John Doe')] #[Example('Jane Smith')] public string $name;
Integration with Build Process
Add to your package.json scripts:
{
"scripts": {
"types:generate": "php artisan typescript:transform",
"schemas:generate": "php artisan schemas:generate",
"build": "npm run types:generate && npm run schemas:generate && vite build"
}
}
Advanced Usage
Custom Validation Mapping
Override or extend validation attribute mappings:
// config/data-schemas.php 'validation_mapping' => [ CustomValidationAttribute::class => function ($attr) { return ['pattern' => $attr->getPattern()]; }, ],
Custom Path Generator
Implement the PathGenerator interface:
use Schemastud\DataSchemas\PathGenerators\PathGenerator; class ApiPathGenerator implements PathGenerator { public function getSchemaPath(ReflectionClass $class): string { return resource_path('api/schemas/' . $class->getShortName() . '.json'); } }
Register in config:
'path_generator' => App\Generators\ApiPathGenerator::class,
Custom Generator
Implement the Generator interface for custom schema formats. Besides canGenerate() and
generate(), a generator returns request, response and strict-LLM variants of itself:
use ReflectionClass; use Schemastud\DataSchemas\Generators\Generator; class OpenApiGenerator implements Generator { private string $mode = 'collapsed'; /** @param array<string, mixed> $config the `data-schemas` config, passed in by the generator chain */ public function __construct(private array $config = []) {} public function canGenerate(ReflectionClass $class): bool { return str_starts_with($class->getName(), 'App\\Api\\'); } public function generate(ReflectionClass $class): array { // Generate an OpenAPI schema for $class, honouring $this->mode return ['type' => 'object', 'title' => $class->getShortName()]; } public function forRequest(): static { return $this->schemaMode('request'); } public function forResponse(): static { return $this->schemaMode('response'); } public function forLlmStrict(): static { return $this->schemaMode('llm_strict'); } public function schemaMode(string $mode): static { $clone = clone $this; $clone->mode = $mode; return $clone; } }
Register it in config. The first generator whose canGenerate() accepts a class handles it, so put
a narrower generator before the default:
'generators' => [ App\Generators\OpenApiGenerator::class, Schemastud\DataSchemas\Generators\JsonSchemaGenerator::class, ],
Registries
This package declares six registries. The schemas cluster page shows how they fit together.
schemas.strategies: the ordered strategies every Data property passes through on its way into a JSON Schema.schemas.projection: where your schemas come from, one source per way of finding the Data classes to project. The package registers thepath-scansource.schemas.id-parsers: how a package's own spelling of a schema reference becomes a full schema URI.schemas.overlays: overlay documents folded over a schema or a record for one tenant, locale or context.schemas.lenses: the declared two-way projections between a canonical record and a rendering of it.schemas.fixtures: named fixture states for a Data class.
It also registers into one registry it doesn't own:
pipelines, fromrushing/laravel-pipeline-registry: theresources:schemastudpipeline, merged fromconfig/pipelines/resources.phpwhen that package is installed.
Requirements
- PHP 8.2+
- Laravel 11.0+ or 12.0+
- spatie/laravel-data 4.0+ or 5.0+
License
The MIT License (MIT). Please see License File for more information.
Credits
- Stephen Rushing
- Inspired by spatie/laravel-typescript-transformer
- Built for the Spatie Laravel Data ecosystem
Strict referenced properties
The forLlmStrict() projection wraps an annotated $ref in a single-branch anyOf, retaining annotations on the outer property. OpenAI rejects annotations directly beside $ref; the equivalent wrapper preserves referenced Data validation and nonnullability. A reference already carrying a sibling anyOf is explicitly refused in strict mode rather than dropping its existing constraint. Ordinary schema modes retain their original reference and annotations. A validator-backed regression checks valid, null and malformed values alongside the provider restriction.
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
Declared morph unions
A closed PropertyMorphableData vocabulary can declare its projection on the base:
use Schemastud\DataSchemas\Attributes\MorphUnion; #[MorphUnion(discriminator: 'kind', arms: [ 'text' => TextNode::class, 'block' => BlockNode::class, ])] abstract class JsonNode extends Data implements PropertyMorphableData { // The ordinary Spatie morph() implementation remains responsible for hydration. }
Arms must be concrete subclasses with a public, non-null string discriminator.
JSON Schema emits oneOf arms combining the class reference with a required literal
const discriminator. Recursive references retain the normal $defs handling.
The attribute describes a closed vocabulary; keep morph() consistent with it.
Open registry-backed morphs are not enumerated. Classes without this attribute
retain their existing schema and transformer behavior.
For Spatie TypeScript Transformer 3.3+, install spatie/typescript-transformer
and register this transformer before your ordinary class/Data transformer:
$config->prependTransformer( \Schemastud\DataSchemas\TypeScript\MorphUnionTransformer::class, );
Include the arm classes in the normal transformation discovery. The extension uses
Spatie's connected references and emits a union of each arm intersected with its
literal discriminator, preserving arm fields and recursive types. It returns
Untransformable for unannotated classes so the configured next transformer runs.
There is no automatic service-provider registration and no change to vendor code.