spatie/laravel-data
Create rich, typed data objects for Laravel that replace form requests and API transformers. Automatically map from requests, validate with inferred rules, transform to resources (with lazy/partial fields), and generate TypeScript definitions from the same source.
Installation
composer require spatie/laravel-data
Publish config (if needed):
php artisan vendor:publish --provider="Spatie\LaravelData\LaravelDataServiceProvider"
Define a Data Object
Create a class extending Spatie\LaravelData\Data:
use Spatie\LaravelData\Data;
class UserData extends Data
{
public function __construct(
public string $name,
public int $age,
) {}
}
First Use Case: Create from Request
$data = UserData::from(request()->all());
// Validates and casts automatically
First Use Case: Transform to JSON
$json = $data->toJson();
// Returns JSON with typed properties
tests/ directory in the package for real-world examples.Replace form requests/API transformers with Data objects:
// Instead of:
class CreateUserRequest extends FormRequest { ... }
// Use:
class UserData extends Data { ... }
$validated = UserData::from(request()->all());
Load nested data on-demand:
class UserData extends Data
{
public function __construct(
public Lazy|PostData $latestPost,
) {}
}
// Access only when needed:
$post = $userData->latestPost; // Loads lazily
Automatic validation via type hints + custom rules:
class UserData extends Data
{
public function __construct(
#[Required]
public string $email,
#[Email]
public string $name,
) {}
public function rules(): array {
return [
'email' => 'unique:users',
];
}
}
Generate frontend types:
php artisan laravel-data:typescript
Outputs UserData.d.ts with inferred types.
class User extends Model
{
use \Spatie\LaravelData\Attributes\WithData;
protected $dataClass = UserData::class;
}
// Usage:
$user = User::find(1);
$data = $user->getData(); // Returns UserData
Replace JsonResource:
class UserResource extends JsonResource
{
public function toArray($request): array
{
return UserData::from($this->resource)->toArray();
}
}
Bind data objects directly to frontend:
// Livewire component
public UserData $userData;
// Alpine.js
const user = @this.userData.toJson();
// Cast: string → Carbon
class DateCast implements Cast
{
public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): Carbon
{
return Carbon::parse($value);
}
}
// Usage:
class EventData extends Data
{
public function __construct(
#[WithCast(DateCast::class)]
public Carbon $date,
) {}
}
Property Name Mismatches
snake_case vs camelCase).#[MapName(SnakeCaseMapper::class)] or #[MapInputName]/#[MapOutputName].class UserData extends Data
{
#[MapName(SnakeCaseMapper::class)]
public string $firstName; // Maps to `first_name` in input/output
}
Lazy Loading Overhead
->load():
$userData = UserData::from(User::with('posts')->find(1));
Validation Rules Not Triggering
rules() method ignored.ValidContext is passed or use validate():
UserData::validate($data); // Explicit validation
TypeScript Generation Conflicts
config/laravel-data.php:
'typescript' => [
'exclude' => [
'App/Data/Internal/*',
],
],
Inspect Data Structure
Use ->toArray() or ->toJson() to debug:
dd($data->toArray());
Validation Errors Check raw errors:
try {
$data = UserData::from($request->all());
} catch (\Spatie\LaravelData\Exceptions\ValidationException $e) {
dd($e->errors());
}
Property Mappers Verify mapper behavior:
$data = UserData::from(['firstName' => 'John']);
dd($data->first_name); // Check if mapped correctly
Custom Property Mappers
Extend Spatie\LaravelData\Mappers\Mapper:
class CustomMapper implements Mapper
{
public function map(string $name): string
{
return strtoupper($name);
}
}
Dynamic Data Classes
Use #[DataClass] attribute for dynamic generation:
#[DataClass]
class DynamicUserData extends Data
{
public function __construct(
public string $name,
) {}
}
Package Integration For testing, include the service provider:
// In TestCase
protected function getPackageProviders($app)
{
return [
\Spatie\LaravelData\LaravelDataServiceProvider::class,
];
}
Avoid Over-Casting
// Bad: Casts every time
#[WithCast(DateCast::class)]
public Carbon $date;
// Good: Cast only during creation
public static function fromArray(array $data): static
{
return new self(
date: Carbon::parse($data['date']),
);
}
Lazy Properties
public function __construct(
public Lazy|function() => array $expensiveData,
) {}
TypeScript Generation
'typescript' => [
'exclude' => [
'App/Data/BigDataClass',
],
],
Disable Validation Skip validation during creation:
UserData::from($data, validate: false);
Custom Validation Messages
Override in rules():
public function rules(): array
{
return [
'email' => ['required', 'email', 'unique:users'],
];
}
public function messages(): array
{
return [
'email.unique' => 'This email is already taken!',
];
}
Global Defaults
Set defaults in config/laravel-data.php:
'default' => [
'validate' => true,
'map_property_names' => true,
],
How can I help you explore Laravel packages today?