Search by

rushing / laravel-data-schemas

stephenr85

This package is abandoned and no longer maintained. The author suggests using the schemastud/laravel-data-schemas package instead.

Generate JSON Schemas from Laravel Data objects with validation attributes

Package info

github.com/schemastud/laravel-data-schemas

pkg:composer/rushing/laravel-data-schemas

Statistics

Installs: 5 374

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 1

v1.1.1 2026-07-12 18:41 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:generate similar to typescript: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 the path-scan source.
  • 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, from rushing/laravel-pipeline-registry: the resources:schemastud pipeline, merged from config/pipelines/resources.php when 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

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.