JSON Schema
Roma builds Illuminate\JsonSchema types from a class at runtime, so the class you map tool
input into also describes that input to the model. Nothing is generated or written to disk.
use BYanelli\Roma\JsonSchema\ClassSchema;
ClassSchema::properties(SearchInput::class); // array<string, Type>, keyed by input key
ClassSchema::object(SearchInput::class); // one ObjectType
properties() returns what the schema() method of a Laravel MCP or AI SDK tool returns:
use App\Tools\SearchInput;
use BYanelli\Roma\JsonSchema\ClassSchema;
use Illuminate\Contracts\JsonSchema\JsonSchema;
class SearchTool extends Tool
{
public function schema(JsonSchema $schema): array
{
return ClassSchema::properties(SearchInput::class);
}
}
The schema describes the class as it is mapped from a plain array, so a property with an
HTTP source attribute such as #[Query] throws. Map the tool's arguments onto the same class
with Mapper::fromArray(SearchInput::class, $arguments) (see
Mapping outside a request).
Types
| PHP type | JSON Schema |
|---|---|
string, int, float, bool |
string, integer, number, boolean |
| a date | string with format: date-time |
a date with #[DateFormat('Y-m-d')] |
string with format: date |
a date with another #[DateFormat] |
string, with the PHP format in the description |
CarbonInterval, DateInterval, ByteSize |
string |
| a backed enum | enum of its values (string or integer) |
| a unit enum | enum of its case names |
| a class | object |
list<T>, array<T>, array<int, T> |
array with items |
non-empty-list<T>, non-empty-array<T>, non-empty-array<int, T> |
array with items and minItems: 1 |
array<string, T> |
object, with the value type in the description |
array{a: int, b?: string} |
object with properties |
| a discriminated union | anyOf, one object per member |
mixed or no type |
any JSON type |
Each member of a discriminated union has
the discriminator key as a required property whose enum is its one value:
{
"anyOf": [
{
"type": "object",
"properties": { "type": { "type": "string", "enum": ["Card"] }, "last4": { "type": "string" } },
"required": ["type", "last4"]
},
{
"type": "object",
"properties": { "type": { "type": "string", "enum": ["bank"] }, "iban": { "type": "string" } },
"required": ["type", "iban"]
}
]
}
A property is required when it has no default, isn't nullable, isn't #[Optional] and
isn't typed with Missing, as when mapping. As in the mapper, a required array or object
may be empty: {"tags": []} is valid for a required list<string>, and non-empty-list<T> adds
minItems: 1 (see Empty lists and
objects). A nullable property allows null. A default that JSON can represent, such as a
scalar, an enum or an array of them, becomes the schema's default.
Dotted keys under one parent, such as #[Key('error.message')] and #[Key('error.code')],
become one object property, error, which is required when one of its members is.
Descriptions
#[Description] on a class or property sets the schema's description. On a property it
replaces the description of the property's class. The TypeScript generator writes the same
text as a doc comment.
use BYanelli\Roma\Attributes\Description;
#[Description('Search the catalog.')]
readonly class SearchInput {
public function __construct(
#[Description('Words to match against titles.')]
public string $query,
public int $limit = 10,
) {}
}
Validation rules
Rules from #[Rule] that have a JSON Schema equivalent add it:
| Rule | Keyword |
|---|---|
min, max, between, size |
minLength/maxLength for a string, minimum/maximum for a number, minItems/maxItems for an array |
in, Rule::in() |
enum |
email, url, uuid |
format: email, uri, uuid |
date, date_format |
format: date-time, or as for #[DateFormat] |
regex |
pattern, without the PHP delimiters |
required, present |
required |
Rules from #[RulePerItem] add their keywords to the schema of the array's items, so
#[RulePerItem('email')] on a list<string> gives items format: email.
Other rules, closure rules and a regex with modifiers other than u are left out of the
schema. The mapper still enforces them when it validates the input.
Limitations
These follow from what Laravel's JsonSchema types support:
- A map such as
array<string, int>is anobjectwith no schema for its values, sinceObjectTypecan only setadditionalPropertiestofalse. Its description gives the value type instead, after any#[Description]text: "Values: integer", "Values: Address" or "Values: array of string". Scalars use JSON type names and classes their short names. - A
non-empty-array<string, T>ornon-empty-array{...}is anobjectwith nominProperties, whichObjectTypedoesn't have. The mapper still rejects{}. - A class that contains itself, such as
list<Comment>inComment, throws, since there is no$refto refer back to it. - A file upload throws, since MCP and AI tool arguments are JSON, which can't carry an uploaded file.
- A
regexis passed through as written after the delimiters are removed, so PHP-only syntax such as\Aor possessive quantifiers isn't converted.