uni-method/jsonapi-mapper
A Laravel package for mapping Eloquent models to JSON:API resources. Define resource classes covering attributes, relationships and links, then transform models into spec-compliant JSON:API documents with consistent, predictable serialization.
This is a micro-package (3 stars, no dependents, single maintainer org). Treat the first hour as due diligence, not integration.
1. Install and pin
composer require uni-method/jsonapi-mapper
Before writing any code, open vendor/uni-method/jsonapi-mapper/ and read three things in this order:
composer.json — PHP constraint, autoload prefixes, and whether it declares a Laravel service provider / facadesrc/ — the entire source is likely a handful of classes; read all of ittests/ (or its absence) — no tests is a strong signal about edge-case behavior2. Find the entry point
You are looking for the mapper's primary public API: typically a Mapper/JsonMapper class with a method that takes a decoded array or JSON string plus a target class name, and returns a hydrated object. If the package ships a Laravel provider, check config/ for published options; if it does not, you will instantiate the mapper yourself.
3. First use case: hydrate one DTO at the HTTP boundary
// app/Dto/OrderDto.php
final class OrderDto
{
public function __construct(
public readonly string $id,
public readonly string $status,
public readonly ?string $customerEmail = null,
) {}
}
// app/Http/Controllers/WebhookController.php
$dto = app(JsonMapper::class)->map(
$request->json()->all(),
OrderDto::class,
);
return response()->noContent();
Verify behavior in Tinker before wiring it into a controller — mapping libraries differ sharply on missing keys, type coercion, and nested objects, and you want to see those failures on a REPL, not in a webhook handler.
Keep the mapper at the boundary only. Do not let mapped DTOs leak into Eloquent models, jobs, or domain services as the canonical representation. Map incoming JSON → DTO → domain command, and domain object → array → JsonResource on the way out. If you find yourself calling the mapper inside a service class, you have coupled your domain to a 3-star dependency.
Bind it, don't new it. Whatever the mapper's constructor looks like, register it once:
// app/Providers/AppServiceProvider.php
$this->app->singleton(\UniMethod\JsonApiMapper\Mapper::class, function () {
return new \UniMethod\JsonApiMapper\Mapper(/* options */);
});
A container binding is your escape hatch: when the package stalls (see Gotchas), you swap the binding for an adapter over cweiske/jsonmapper or spatie/laravel-data and touch one file instead of fifty.
Wrap it in a thin application-level adapter. Even if the package API is convenient, define your own JsonHydrator interface with hydrate(array $payload, string $class): object and implement it with the package. This costs ~20 lines and buys you testability, since you can fake the hydrator in feature tests without the package's quirks.
Per-endpoint DTOs, one mapper config. Different integrations have different casing and nullability conventions. Rather than trying to configure one global mapper for everything, keep DTO classes explicit per integration (Stripe/InvoiceDto, Internal/InvoiceDto) and let the mapper's default behavior handle the common case. Explicit constructors with readonly properties give you PHP-level validation for free.
Validate before you map. The mapper converts shape; it does not enforce business rules. Run Validator::make($request->all(), $rules) first, then map the validated array. Mapping unvalidated input means a malformed payload surfaces as a TypeError deep inside the package with a useless stack trace.
Test the round trip, not the internals. Assert on the DTO's property values and on the JSON your API returns. Do not unit test the mapper itself — you do not own it, and its behavior may change when you eventually replace it.
Maintenance risk is the headline. Three stars and zero dependents means you are likely one of a very small number of users. Before shipping, check the last commit date and open issues. If the repo has been quiet for 12+ months, plan for one of: (a) vendor the source into app/Support/JsonMapper/ under your own namespace, (b) wrap it behind the adapter interface above so replacement is cheap, or (c) skip it entirely and use symfony/serializer or spatie/laravel-data, both of which handle the same job with real maintenance and Laravel-native ergonomics.
Type coercion is where mappers silently lie. Confirm what happens when JSON contains "42" for an int property, null for a non-nullable one, or a key that is absent versus present-and-null. Many small mappers use reflection without strict types and will happily assign "42" to an int $id — which then fails much later. If the package has no strict mode, add a post-map assertion pass, or cast explicitly in your DTO's fromArray static.
Nested objects and collections are usually the weakest area. Test a nested object and a list<Dto> in Tinker early. If arrays-of-objects are unsupported, you will need a manual loop, and that is a good moment to reconsider the dependency.
Mass-assignment surface. A reflection-based mapper may write to any property it can name, including ones you did not intend to expose. Keep DTOs readonly and constructor-only so unknown keys become an error rather than a silent write. Never map directly into an Eloquent model.
Casing mismatches. PHP properties are typically camelCase; JSON payloads are often snake_case. Find the package's key-transformation option (or custom name-mapping hook) before hand-writing accessor boilerplate in every DTO.
Dates and enums. Verify whether the mapper handles DateTimeImmutable and PHP backed enums, or whether it requires a string property plus a cast in your DTO. This is a frequent gap in minimal mappers.
Debugging technique. When a hydration fails, dump both sides:
dump($payload, $dto ?? null); // then dd() the caught exception
Most failures are a key-name mismatch or a null where the constructor forbids it — the package's exception message often will not say which. Wrap the call in try/catch (\Throwable $e) in your adapter and rethrow with the payload keys attached so the log is actionable.
Extension points to look for. If the mapper offers custom type handlers, normalizers, or a map() callback per property, that is your lifeline for special cases (money objects, UUID value objects). If it offers none, budget for pre-processing the array into a flat, mapper-friendly shape before hydration — that preprocessor is where all your integration-specific weirdness should live.
Lock the version exactly. With no dependents, semantic-versioning discipline is unproven. Use "uni-method/jsonapi-mapper": "1.0.*" or a commit hash rather than ^1.0, and commit composer.lock.
How can I help you explore Laravel packages today?