Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Data Laravel Package

spatie/laravel-data

Spatie Laravel Data lets you define rich data objects once and reuse them as form requests, API transformers, and TypeScript definitions. It supports automatic validation, resource transformation, lazy properties, and constructing objects from request data.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and do almost nothing else first. composer require spatie/laravel-data. The service provider is auto-discovered, and the defaults are sane. Resist publishing the config on day one — add it only when you actually need to change something (php artisan vendor:publish --provider="Spatie\LaravelData\LaravelDataServiceProvider" --tag="laravel-data-config" → config/data.php). The three keys you will eventually touch are validation_strategy (always / only_requests / never), name_mapping_strategy (input/output key casing), and casts.

Read in this order. The docs site (/v4/introduction) — specifically Creating data objects → Validation → Transforming → Eloquent casts → TypeScript. Skip the advanced sections (morphs, Livewire synths, custom normalizers) until you hit a wall; they are 20% of the docs covering 2% of daily use.

First use case: kill one FormRequest + one API Resource on a single endpoint. Don't refactor the app. Pick one POST route:

class SongData extends Data
{
    public function __construct(
        public string $title,
        public string $artist,
        #[Min(1), Max(10)] public int $rating,
    ) {}
}

Then in the controller: $data = SongData::validateAndCreate($request); and return SongData::from($song) from the store/show methods. That's the whole value proposition in ten lines: one class is now your validation, your DTO, and your response shape. If that feels good, keep going; if the typing friction in your IDE bothers you, stop there and don't migrate the rest of the codebase.

Two commands to learn immediately:

php artisan data:cache     # warm the reflection metadata cache
php artisan data:clear

And if you want the TypeScript payoff (php artisan typescript:transform, requires spatie/typescript-transformer), wire it into CI from day one — otherwise generated types drift and nobody trusts them.

Implementation Patterns

The request → data → action → model pipeline. The idiomatic flow replaces the FormRequest/Resource sandwich:

// Controller
public function store(SongData $data)          // resolved + validated from the request
{
    $song = Song::create($data->all());        // all() = raw properties, no transformation
    return SongData::from($song);              // Data implements Responsable → JSON response
}

Type-hinting the data class in a controller method (v4 resolves it from the request, validation included) is the pattern that saves the most code. #[FromRouteParameter('song')] pulls route params into the same object, so PATCH /songs/{song} gets you both the model key and the validated body in one signature.

Validation: let inference do the work, then escape to rules(). PHP types give you required, string, integer, array, enum membership, and nested object validation for free. #[Min], #[Max], #[Email], #[Regex], #[In], #[Exists], #[Unique], #[Sometimes], #[Nullable] cover the rest. Use a public static function rules(): array only for genuinely dynamic constraints (Rule::unique(...)->ignore($this->id) style), and keep in mind attribute-level rules apply to the property, class-level rules() applies to the payload.

Distinguish "missing" from "null" with Optional. This is the single most useful feature for PATCH endpoints:

class UpdateSongData extends Data
{
    public function __construct(
        public Optional|string $title,
        public Optional|string|null $artist,
    ) {}
}

$data->title is an Optional instance when the key is absent, so if ($data->title instanceof Optional) means "don't touch this column". Without it you cannot tell a partial update from a deliberate null.

Lazy properties for expensive relations, not as a default. Lazy::create(fn () => ...) and Lazy::whenLoaded('artist', $song, fn () => ArtistData::from($song->artist)) let one data class serve both list and detail endpoints. #[AutoWhenLoaded] (and #[AutoLazy] since 4.13) automate the common Eloquent case. The discipline that makes this work: eager-load in your query, then let the data class decide what to emit.

DataCollection for lists and pagination. SongData::collect($songs) returns a collection whose toArray() is a plain array of transformed objects; SongData::collect(Song::paginate()) returns a paginated collection that serialises with data/links/meta intact and can be returned straight from a controller. Subclass DataCollection when you need shared behaviour, and annotate with #[DataCollectionOf(SongData::class)] so rule inference and TypeScript generation know the item type.

Eloquent casts for JSON columns. This is the pattern teams underuse:

protected function casts(): array
{
    return ['settings' => \Spatie\LaravelData\Casts\AsData::class];
}

Now $model->settings is a typed object, $model->settings->theme autocompletes, and dirty-checking works (v4.16 added compare() to the casts for isDirty). Use AsDataCollection for arrays of objects. Pair with a json column and never hand-decode again.

Custom casts and transformers at the property level. #[WithCast(DateTimeInterfaceCast::class, format: 'Y-m-d')] for non-ISO dates, #[WithCast(EnumCast::class)] for cross-enum casting, #[WithTransformer(...)] for output-only reshaping. Keep these on the property, not in a global config, so the class is self-describing for the next developer.

Name mapping is a class-level decision, not a per-property one. Frontend in camelCase, DB in snake_case: #[MapOutputName(SnakeCaseMapper::class)] or set name_mapping_strategy once in config/data.php and stop thinking about it. Use #[MapInputName] when the inbound payload differs from the outbound one.

Lock the contract with feature tests. The highest-leverage habit with this package: an assertion on the exact JSON of a few representative endpoints, plus ->only([...])/->except([...]) in tests to force yourself to notice when a new property leaks into an API response. Add a CI check that php artisan typescript:transform produces no diff.

Gotchas and Tips

  • The metadata cache is a deploy hazard. php artisan data:cache writes to bootstrap/cache, and stale caches after renames, moved namespaces, or package upgrades cause genuinely baffling behaviour (see the 4.14 changelog warning). Add data:clear + data:cache to your deploy script after composer dump-autoload, and make sure data:clear runs before it. If something "makes no sense", clearing this cache is the first debugging step.

  • Type inference cannot see your database. string $email gives you string, not unique:users. Every DB-level constraint needs an explicit attribute or a rules() override — don't assume the inferred rule set is "complete validation".

  • Nested collections need the annotation. A DataCollection $songs property without #[DataCollectionOf(SongData::class)] will validate weakly and generate useless TypeScript. Same for array properties where you expect element validation.

  • Validation cost is real. Rule inference is memoised per class, but validateAndCreate on a large nested object is measurably slower than a hand-written FormRequest (4.18 shipped a performance pass for exactly this). If you have hot endpoints, benchmark before converting them, and prefer Data::from($array) + explicit Validator::make inside actions when the payload is trusted (e.g. an internal job).

  • Optional requires the union type. public Optional|string $name works; public string $name with a #[Sometimes] attribute throws on missing keys. The moment you add Optional, every consumer must handle the wrapper — grep for the property before shipping.

  • toArray() vs all(). all() returns the raw property values (including nested Data objects); toArray()/transform() run the transformation layer (casts, name mapping, lazy resolution, grouping, wrapping). Use all() inside actions and toArray() at the boundary. Mixing them is the source of most "why is this a string here and an object there" confusion.

  • Name-mapping surprises in both directions. A global name_mapping_strategy applies to input and output; a frontend sending camelCase to a snake_case-mapped class will silently fail validation as "missing required field". When debugging a validation failure, dump SongData::getValidationRules($payload) (or the failing ValidationException's bag) rather than guessing.

  • Lazy is not a query optimiser. Lazy::whenLoaded still depends on with(['artist']) in your query builder. A missing eager load yields a missing key (or an exception, depending on the version — 4.15.2 fixed a CannotCreateData on unloaded lazy relations). Assert with DB::listen or assertQueryCount in tests, especially on index endpoints.

  • Recursion and depth. Circular relations plus #[AutoWhenLoaded] can recurse forever. max_transformation_depth and throw_when_max_transformation_depth_reached are the guardrails; turn throwing on in local/CI so you notice instead of getting a silently truncated payload.

  • Dates and enums are the usual cast bugs. Non-ISO date strings need an explicit DateTimeInterfaceCast with a format; nanosecond/timezone handling has had several point fixes (4.21). Enum casting from int↔string enums is supported, but mismatched backing types throw CannotCastEnum — read the exception, it names the property (4.16).

  • Extension points, in order of how often you'll need them: custom Casts implementation (cast()/get()) for value objects and encrypted fields; custom Transformer for TypeScript output of those value objects; a custom Normalizer if you need to accept multiple input shapes; TransformationContext when you need per-request control over only/except/lazy resolution instead of hard-coding wrap()/only() in the class.

  • Escape hatches exist — use them. For a gnarly legacy endpoint, Data::from($array)->transform() inside a resource or a manually built array is fine. The package is at its best as an opt-in layer, and at its worst when a team forces every array in the app through a Data subclass.

  • Overall assessment: with 1.8k stars and a steady v4 minor cadence from Spatie, this is a low-risk dependency to standardise on provided you treat it as three features (validation, transformation, TypeScript) rather than a religion. The reflection-heavy "describe once" magic is what buys the leverage — and is also where the debugging time goes. Start with one endpoint, add data:cache to deploys, and add the typescript:transform diff check before the codebase gets big enough that drift becomes invisible.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib