waleedhu/rules-to-schema

Build JSON Schema from Laravel validation rules.

Maintainers

Package info

github.com/WaleedHU/rules-to-schema

pkg:composer/waleedhu/rules-to-schema

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-08-03 22:35 UTC

This package is auto-updated.

Last update: 2026-08-03 22:39:11 UTC


README

Build JSON Schema from Laravel validation rules.

You already describe the shape of your data once, in validation rules. This turns that description into a JSON Schema, so you don't write it a second time to define an LLM tool, a structured output format, or an OpenAPI body.

composer require waleedhu/rules-to-schema

Requires PHP 8.3+ and Laravel 13's illuminate/json-schema.

Usage

use WaleedHu\RulesToSchema\RuleSchema;

$schema = RuleSchema::fromRules([
    'title'  => 'required|string|min:3|max:120',
    'status' => 'required|in:draft,published,archived',
    'author.name'  => 'required|string|max:50',
    'author.email' => 'required|email',
    'rating' => 'nullable|numeric|between:0,5|multiple_of:0.5',
    'tags'   => 'array|max:5',
    'tags.*' => 'string|max:20',
]);

$schema is an Illuminate\JsonSchema\Types\ObjectType, so toArray() gives you a PHP array and casting to string gives you JSON:

{
    "properties": {
        "title": { "minLength": 3, "maxLength": 120, "type": "string" },
        "status": { "enum": ["draft", "published", "archived"], "type": "string" },
        "author": {
            "properties": {
                "name": { "maxLength": 50, "type": "string" },
                "email": { "format": "email", "type": "string" }
            },
            "type": "object",
            "required": ["name", "email"]
        },
        "rating": { "minimum": 0, "maximum": 5, "multipleOf": 0.5, "type": ["number", "null"] },
        "tags": { "maxItems": 5, "items": { "maxLength": 20, "type": "string" }, "type": "array" }
    },
    "type": "object",
    "required": ["title", "status"]
}

Note that author itself is optional here, because no rule marks it required — but name and email are required within it if it is present. Add 'author' => 'required' to require the object too.

Reusing a form request

Rules live in a form request more often than in an array, and they work just as well:

RuleSchema::fromRules((new StorePostRequest)->rules());

Nesting and arrays

Dot notation becomes nested objects, and * becomes array items:

RuleSchema::fromRules([
    'attachments'        => 'array',
    'attachments.*.url'  => 'required|url',
    'attachments.*.size' => 'integer|min:1',
]);
{
    "properties": {
        "attachments": {
            "items": {
                "properties": {
                    "url": { "format": "uri", "type": "string" },
                    "size": { "minimum": 1, "type": "integer" }
                },
                "type": "object",
                "required": ["url"]
            },
            "type": "array"
        }
    },
    "type": "object"
}

What gets mapped

Only rules with an exact JSON Schema counterpart.

Rules Becomes
string, integer, numeric, boolean, array, list, decimal type
required the parent's required list
nullable "null" added to type
min, max, size, between minLength/maxLength, minimum/maximum, or minItems/maxItems
multiple_of multipleOf
in enum
regex pattern
email, url, uuid, ipv4, ipv6, date format

Bounds are resolved in the unit the type is measured in, the same way the validator sizes a value: min:3 is minLength on a string, minimum on a number, and minItems on an array. A field with no type rule is treated as a string, which is also how the validator sizes an untyped value.

What gets ignored

Anything without an exact counterpart is skipped rather than approximated, so the schema never claims a constraint the rules don't express:

  • Rules with no equivalent, such as confirmed, exists and mimes.
  • ip, which accepts either version, so it matches neither the ipv4 nor the ipv6 format.
  • ulid, because a ULID is not a UUID.
  • Rule objects, closures and invokable rules, whose logic isn't inspectable.

Testing

composer test

License

MIT.