Skip to content

Field types reference

Every concrete field type inherits the shared fluent surface documented in fieldsmake(), 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.
Str::make('title')->required()->maxLength(200)->sortable();

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.
Decimal::make('averageRating')->readOnly()->nullable();

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.

Boolean::make('explicit');

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.

Date::make('birthDate')->nullable();

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.

Time::make('previewOffset')->nullable();

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.

Map::make('releaseInfo')->fields(
    Str::make('label'),
    Str::make('catalogueNumber')->readOnly(),
);

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 PATCH merges per-child — an un-supplied child keeps its stored value (the single-value twin of Map's per-column merge);
  • an explicit null clears 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 (a DateTime inside 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:

ArrayHash::make('preferences')->minProperties(0)->maxProperties(20)->sortKeys();

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:

Email::make('email')->required()->strict();
Ip::make('lastSeenIp')->nullable();

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 notStr, DateTime, BelongsTo and HasMany — are non-final by design, because each already has a shipped subclass that extends it: StrEmail/Url/Uuid/ Slug/Ip, DateTimeDate/Time, BelongsToHasOne, and HasManyBelongsToMany. 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 Id field and the id lifecycle (it is special-cased into the top-level id, 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.