Field types reference¶
Every concrete field type inherits the shared fluent surface documented in
fields — make(), storedAs(), the read-only and constraint
shortcuts, the four serialize/hydrate hooks. This page is the per-type reference:
each section shows only the delta a type adds on top of that surface — its
casting behaviour and its type-specific constraint helpers. Reach for it when you
are picking the right field for a member.
All the snippets below are lifted from the music-catalog example app — the
TrackResource,
AlbumResource and
UserResource between
them exercise every type here.
Str¶
A generic string attribute, and the base class for the format subtypes below. Adds length, pattern and the five format shortcuts.
| Method | Adds |
|---|---|
minLength(int) / maxLength(int) |
MinLength / MaxLength. |
pattern(string $regex) |
Pattern. |
email(bool $strict = false) |
EmailFormat. |
url(array $allowedSchemes = []) |
UrlFormat. |
uuid(?int $version = null) |
UuidFormat. |
slug(?string $regex = null) |
SlugFormat. |
ip(?int $version = null) |
IpFormat. |
The five format shortcuts produce exactly the same constraint metadata as the
dedicated Email / Url / Uuid / Slug / Ip
field types — Str::make('email')->email() is interchangeable with
Email::make('email'). Use the shortcut when the field also carries other string
rules in the same chain; use the dedicated type when the field is only that
format. (This equivalence is stated once, here.)
Integer¶
An integer attribute (JSON type: integer); casts to int on both serialize and
hydrate. The trackNumber
and durationSeconds members are integers — the wire value 1 round-trips to a
PHP int.
| Method | Adds |
|---|---|
min(int) / max(int) |
Min / Max. |
exclusiveMin(int) / exclusiveMax(int) |
ExclusiveMin / ExclusiveMax. |
multipleOf(int) |
MultipleOf. |
in(array $values) |
In. |
Integer::make('trackNumber')->min(1)->sortable();
Integer::make('durationSeconds')->storedAs('length_seconds');
Decimal¶
A floating-point attribute (JSON type: number); casts to float. The bound
helpers accept int|float. The album's
averageRating is a
Decimal (a nullable, read-only rating).
| Method | Adds |
|---|---|
min(int\|float) / max(int\|float) |
Min / Max. |
exclusiveMin(int\|float) / exclusiveMax(int\|float) |
ExclusiveMin / ExclusiveMax. |
multipleOf(int\|float) |
MultipleOf. |
in(array $values) |
In. |
Boolean¶
A boolean attribute; casts to bool. No type-specific constraint helpers — its
whole job is the bidirectional bool cast. The track's
explicit flag is a
Boolean.
DateTime¶
DateTime serializes a \DateTimeInterface to a string and hydrates a string
back to a \DateTimeImmutable. The default format is ISO-8601
(\DateTimeInterface::ATOM).
| Method | Adds / effect |
|---|---|
format(string) |
Override the serialization format string (default \DateTimeInterface::ATOM). |
before($bound) / after($bound) |
Before / After. |
between($min, $max) |
Between. |
useTimezone(string) |
Convert hydrated values into the given timezone before storing. |
before(), after() and between() accept either a fixed \DateTimeInterface
or a \Closure(): \DateTimeInterface. A closure bound is evaluated at
validation time, so it reflects the moment of the request — and because it is
opaque PHP, it does not round-trip to JSON Schema.
The album's releasedAt
is the worked example: a closure before bound forbidding future release dates,
plus a timezone normalisation to UTC.
DateTime::make('releasedAt')
->before(static fn(): \DateTimeImmutable => new \DateTimeImmutable())
->useTimezone('UTC')
->sortable();
Because new \DateTimeImmutable() is resolved each time the rule runs, "no future
releases" always means now, per request — not the moment the resource class was
loaded. The same closure form works for after() and for either bound of
between().
Date¶
A DateTime fixed to the Y-m-d format — a calendar date with no time
component. It inherits every DateTime helper. The user's
birthDate is a
nullable Date.
Time¶
A DateTime fixed to the H:i:s format — a wall-clock time. The track's
previewOffset (the
offset into the track where its preview starts) is a nullable Time.
Map¶
Map exposes a nested JSON object in the resource attributes while spreading its
values across multiple flat columns on the same domain object. Each child
field reads and writes its own column; the child's name is the key inside the
nested object. Declare the children with fields().
| Method | Effect |
|---|---|
fields(FieldInterface ...$children) |
Declares the child fields. Each child maps to its own flat column. |
The album's releaseInfo
is the worked example: it presents as a nested object { "label": …,
"catalogueNumber": … }, but the two children read and write the flat label and
catalogueNumber columns on the Album.
The catalogueNumber child is readOnly(), so a value supplied through the
nested object on write is ignored.
Top-level constraints on a Map are limited to presence — required() and
nullable(). Every structural rule belongs on a child field, and a child
violation surfaces as a nested
/data/attributes/releaseInfo/<child> source pointer (so a bad catalogueNumber
points at /data/attributes/releaseInfo/catalogueNumber, not at the map).
A read-only child is skipped on hydration per the operation context, exactly as a
top-level field is — a readOnly() child can't be written through the nested
object on create or update, and readOnlyOnCreate()/readOnlyOnUpdate() each gate
only their own context.
Map::on($relation)— spreading children across a related model rather than the same one — is out of scope for core.
Obj¶
A typed nested object stored in a single backing value — one JSON column, one
array property. It is the single-value sibling of Map: where Map spreads its
children across separate flat columns and the nested object is only a wire view,
Obj keeps the whole object as one value and its children address keys inside
it. Use it when a structured attribute's natural storage is a single JSON document.
| Method | Effect |
|---|---|
fields(FieldInterface ...$children) |
Declares the child fields. Each child reads and writes a key inside the single value. |
children() |
The declared children (for adapters and projection). |
Obj::make('address')->nullable()->fields(
Str::make('street')->required(),
Str::make('city')->required(),
Str::make('postcode')->required()->maxLength(10),
);
The child fields carry the typing and the constraints, exactly as Map children
do — a child violation surfaces as a nested /data/attributes/address/<child>
source pointer, and a read-only child is skipped on hydration per the operation
context. Top-level constraints on the Obj itself are limited to presence
(required() / nullable()).
Write semantics on the single value:
- a partial
PATCHmerges per-child — an un-supplied child keeps its stored value (the single-value twin ofMap's per-column merge); - an explicit
nullclears the whole object; - the field-level
serializeUsing()/fillUsing()hooks remain the escape hatch when a child value needs a richer PHP type than JSON scalars (aDateTimeinside the object, a value object) — the stored children are otherwise plain scalars in one array.
In the generated OpenAPI document an Obj projects a typed object schema built
from its children (it self-describes via ProvidesFieldSchema).
Map vs Obj vs ArrayHash: Map = declared keys, flat columns. Obj =
declared keys, one value. ArrayHash (below) = dynamic keys, one value.
OneOf¶
A discriminated union attribute: one of several declared object shapes
(variants), selected by a discriminator property inside the value, stored — like
Obj — as a single backing value that includes the discriminator.
| Method | Effect |
|---|---|
discriminator(string $property) |
Names the property whose value selects the variant (default type). |
variant(string $name, FieldInterface ...$children) |
Declares a variant: the discriminator value that selects it, and its child fields. |
OneOf::make('block')->nullable()->discriminator('kind')
->variant('heading', Str::make('text')->required(), Integer::make('level')->min(1)->max(6))
->variant('image', Url::make('src')->required(), Str::make('alt'));
Only the selected variant's children run: a heading block validates and
hydrates text/level, an image block src/alt. On a partial PATCH the
incoming discriminator falls back to the stored one — the same variant merges
per-child, a different variant starts fresh (stale keys from the old variant
are not carried over). An unknown or missing discriminator has no variant to
hydrate through, so the raw value is stored for the adapter's validator to reject
(hydration never validates) — the host surfaces it as a 422 pointing at
/data/attributes/<field>/<discriminator>.
In the generated OpenAPI document a OneOf projects oneOf + a discriminator
object, each branch carrying the discriminator as a const (and, on create, in
its required list) — so a consumer can match a value to its variant statically.
For asserting a shape over a free-form value instead of declaring fields —
including unions of raw member schemas — see the
Shape composite-schema constraint.
ArrayList¶
A zero-indexed array attribute (JSON type: array).
| Method | Adds / effect |
|---|---|
minItems(int) / maxItems(int) |
MinItems / MaxItems. |
uniqueItems() |
UniqueItems. |
each(ConstraintInterface ...$constraints) |
Each — applies the given constraints to every item. |
sorted() |
Sorts the list on serialization. |
The track's genres
is the worked example — at least one genre, each a non-empty string, no
duplicates. Note that each() takes constraint value objects (not a nested
field), so the per-item rule is new MinLength(1):
use haddowg\JsonApi\Resource\Constraint\MinLength;
ArrayList::make('genres')
->minItems(1)
->each(new MinLength(1))
->uniqueItems();
ArrayHash¶
A JSON object attribute exposed as a PHP associative array (JSON type: object)
with dynamic keys — the keys are open-ended data, not declared field names.
| Method | Adds / effect |
|---|---|
minProperties(int) / maxProperties(int) |
MinProperties / MaxProperties. |
sortKeys() |
Sort by key on serialization. |
sortValues() |
Sort by value on serialization (keys preserved). |
The user's preferences
is an ArrayHash, sorted by key for a stable wire shape:
Map vs ArrayHash is the key distinction to get right. Both render a JSON
object, but Map has declared keys — each child is its own field, mapping to
its own flat column, validated and cast independently. ArrayHash has dynamic
keys — an open bag of properties stored and emitted as one associative array.
Use Map for a fixed nested shape (an address, release info); use ArrayHash
for arbitrary user-supplied key/value data (preferences, settings).
Email, Url, Uuid, Slug, Ip¶
Each is a Str whose make() pre-attaches the matching format constraint —
so Email::make('email') already carries an EmailFormat before you chain
anything. They are pure sugar over the Str format shortcuts, plus one
type-specific helper each.
| Type | Equivalent to | Extra helper |
|---|---|---|
Email::make($name) |
Str::make($name)->email() |
strict() — opt into RFC-strict validation. |
Url::make($name) |
Str::make($name)->url() |
allowedSchemes(string ...$schemes) — restrict the URI schemes. |
Uuid::make($name) |
Str::make($name)->uuid() |
version(int) — narrow to a UUID version. |
Slug::make($name) |
Str::make($name)->slug() |
— |
Ip::make($name) |
Str::make($name)->ip() |
v4() / v6() / both() — narrow the IP version (default both). |
The user's email and
lastSeenIp demonstrate two of them:
Reconcile, not stack. The extra helpers replace the pre-attached format
constraint rather than adding a second one. Email::make()->strict() does not end
up with two EmailFormat rules — strict() removes the lax one make() attached
and adds a single strict EmailFormat. Likewise Url::make()->allowedSchemes('https')
re-attaches a single UrlFormat with the scheme list, and Ip::make()->v4()
narrows to one IPv4 IpFormat. So a chain like ->strict() always leaves exactly
one format constraint on the field.
Subclassing and finality¶
Most concrete field types are final. The four intermediates that are not —
Str, DateTime, BelongsTo and HasMany — are non-final by design, because
each already has a shipped subclass that extends it: Str ← Email/Url/Uuid/
Slug/Ip, DateTime ← Date/Time, BelongsTo ← HasOne, and HasMany ←
BelongsToMany. The current finality set is therefore deliberate, not an
oversight. Dropping final from one of the remaining leaf types later to allow a
custom subclass is a non-breaking change (it only widens what callers may do),
so the surface can grow toward extensibility without a breaking release.
Next¶
- Fields — the shared fluent surface every type above inherits (naming, visibility, read-only contexts, constraint shortcuts, the four hooks).
- Ids — the
Idfield and the id lifecycle (it is special-cased into the top-levelid, not an attribute). - Relations — the relationship field types and the relation DSL.
- Validation — what the constraint metadata each type adds actually does, and the create/update context model.