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 Package Laravel Package

json-mapper/laravel-package

Map JSON payloads and arrays into typed PHP objects and DTOs with this Laravel package. A fluent API defines field mappings, nested structures, and collections, with clean integration into Laravel's container, facades, and config.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin Laravel integration around the core JsonMapper library — it wires the mapper into the service container, adds a facade/config file, and lets you stop hand-writing $data['user']['address']['street'] ?? null chains. Treat it as infrastructure, not a feature: the value shows up the moment you have more than two external JSON payloads to consume.

Check the support matrix before you install — 3.0.0's changelog contradicts itself. The release notes claim both "Restore support for Laravel 8, 9 and 10" (under Fixed) and "Dropped support for ... Laravel 5.5 through 10. Minimum supported ... Laravel version is now 11" (under Changed). One of those is wrong. Read composer.json in the released tag, not the release notes:

composer show json-mapper/laravel-package --all | grep -E '^(versions|requires)'
# or, before installing:
curl -s https://raw.githubusercontent.com/JsonMapper/LaravelPackage/3.0.0/composer.json | jq '.require'

What is not ambiguous: PHP 7.2/7.3 are gone (floor is 7.4), and Laravel 13 is now supported. If you are on Laravel 11+ you are fine either way. If you are on Laravel 9/10, verify empirically — install into a throwaway app and boot it — because the notes give you no reliable answer.

Install and publish:

composer require json-mapper/laravel-package:^3.0
php artisan vendor:publish --provider="JsonMapper\LaravelPackage\JsonMapperServiceProvider"

Where to look first, in this order:

  1. The released tag's composer.json — resolves the support-matrix ambiguity above in ten seconds.
  2. config/json-mapper.php (whatever the publish tag emits) — namespace/class-map options, middleware stack, and cache settings live here.
  3. The package's service provider — this tells you the exact container binding and facade accessor. It's a small file; read it rather than guessing the alias in app()->make().
  4. The upstream json-mapper/json-mapper README — 90% of your day-to-day API surface (attributes, JsonMapperBuilder, middleware, exceptions) is documented there, not in the Laravel wrapper.

Upgrading from 2.x: this is a major bump. The only stated breaking change is the PHP floor (7.4+) and the Laravel range, but majors in small packages often carry config-file and binding changes that go unlisted. Diff the published config against yours, and if you were tracking dev-develop or dev-master in composer.json (easy to do given how small this package is), switch to a version constraint now — 3.0.0 explicitly removed those branch references, so that constraint is already broken.

First use case — map an inbound webhook to a DTO instead of an array:

final class OrderShipped
{
    public string $orderId;
    public string $trackingNumber;
    public \DateTimeImmutable $shippedAt;
    /** @var OrderLine[] */
    public array $lines = [];
}

class StripeWebhookController
{
    public function __invoke(Request $request, JsonMapperInterface $mapper)
    {
        $event = $mapper->mapToObject($request->getContent(), new OrderShipped());

        ProcessShipment::dispatch($event);

        return response()->noContent();
    }
}

That's the whole pitch: the controller gets one typed, autocompletable object, and everything downstream stops doing defensive array access.

Implementation Patterns

One DTO per external contract, never per database table. Keep mapping DTOs in app/Data/ or app/Integrations/{Vendor}/ and let them drift independently from Eloquent models. The mapper's job ends at "valid PHP object"; persisting is a separate explicit step.

$order = Order::create([
    'external_id'     => $dto->orderId,
    'tracking_number' => $dto->trackingNumber,
    'shipped_at'      => $dto->shippedAt,
]);

Doing it this way means a vendor renaming a field breaks exactly one DTO, not your whole model layer.

Inject the interface, not the facade. Type-hint JsonMapperInterface in constructors and controller actions. It keeps your services testable with a fake, and it survives the package swapping its facade alias — which is exactly the class of change that ships unannounced in a major like 3.0.0.

Normalize naming at the mapping layer. Vendor APIs are almost always snake_case; your PHP is camelCase. Use the mapping attributes (or a configured naming strategy/middleware if the wrapper exposes one) rather than writing transform loops:

use JsonMapper\Attribute\MapFrom;

final class Customer
{
    #[MapFrom('customer_id')]
    public string $customerId;

    #[MapFrom('billing.address.line1')]
    public ?string $addressLine1 = null;

    /** @var Subscription[] */
    #[MapFrom('subscriptions')]
    public array $subscriptions = [];
}

Nested types need real type information. array $lines alone gives you arrays of stdClass or arrays of arrays. Declare OrderLine[] in the docblock and make the element class exist as a typed property or attribute — reflection is what drives recursion.

Validate before you map, or map into a validated shape. Two viable orderings:

  • FormRequest rules on the raw payload → map the validated array into the DTO. Good for user-facing endpoints where you want 422s with field errors.
  • Map first → validate the DTO with Laravel's Validator::make((array) $dto, ...) or a dedicated validator. Good for webhooks, where the payload shape is contractual and a mapping failure is a 400/log-and-alert, not a user error.

Don't do both halfway; pick one and be consistent per integration.

Queue boundaries. Serialize DTOs, don't pass raw arrays through jobs — $job->payload being a typed object is the difference between a queue retry that fails loudly and one that silently nulls a field three layers down. If the DTO has readonly/typed properties, PHP's native serialization handles it; just version your DTOs deliberately, because an in-flight job deserializing against a changed class is a classic silent break. A major-version upgrade of the mapper is a fine moment to audit which DTOs are sitting in a queue.

Testing. Keep a tests/Fixtures/{vendor}/*.json corpus of real captured payloads and assert on the mapped object:

$mapper = app(JsonMapperInterface::class);
$dto = $mapper->mapToObject(
    file_get_contents(base_path('tests/Fixtures/stripe/order_shipped.json')),
    new OrderShipped()
);

$this->assertSame('ord_123', $dto->orderId);
$this->assertCount(2, $dto->lines);

That fixture set becomes your regression suite for vendor API changes, and after 3.0.0 it doubles as your regression suite for mapper changes. Run it against the new major before you merge the bump.

Put the framework matrix in CI, not in your head. Given how murky the 3.0.0 support story is, the cheapest durable answer is a matrix job that installs the package at its lowest and highest supported Laravel versions and runs the fixture suite. This is a five-line GitHub Actions addition and it converts "does this still work on L10?" from a research problem into a build result.

Gotchas and Tips

The 3.0.0 changelog is self-contradictory — trust composer.json, not the notes. "Restore support for Laravel 8, 9 and 10" and "minimum supported Laravel version is now 11" cannot both be true. This is a one-person-ish package where release notes are assembled by automation (3.0.0 added changelog/release workflows, which is likely how the conflict got in), so treat every claim in them as a lead to verify rather than a fact. Same applies to the PHP floor: 7.4 is what the notes say, but Laravel 11 requires 8.2, so the real floor for most consumers is 8.2.

Breaking-change surface may exceed what's documented. The stated breaks are PHP + Laravel ranges. Majors in thin wrappers usually also touch the service provider binding, the facade accessor, and the published config's shape — none of which appear in these notes. Diff the config file and the provider against your 2.x copies during the upgrade; it takes two minutes and catches the whole category.

Branch references were removed. If you pinned dev-develop or dev-master (tempting, given the package's size and release cadence), that constraint is now dead. Move to ^3.0 with a composer.lock commit.

Coercion is not validation. Mappers happily turn "0" into 0, "" into null, "yes" into true depending on the converter. A DTO that constructs successfully is not a payload you can trust. Always layer explicit validation or assert()-style guards on top of the mapped object.

Missing keys vs. null are different failures. A required public string $orderId; with an absent key throws; an optional public ?string $note = null; silently succeeds. Decide per field, deliberately, and prefer non-nullable + a default only when the vendor genuinely guarantees the key.

Catch the right exception and translate it. Mapping failures should never surface as 500s. Wrap the map call, catch the library's mapper exception, log the raw payload (redacted), and return 400/422 with a generic message.

Empty arrays are a trap. "lines": [] and "lines": null and a missing lines key need three different behaviours in most integrations. Test all three; the mapper will not choose for you.

Performance and Octane. Reflection-heavy mapping is not free. Keep the mapper as a container singleton (the wrapper does this by default — verify after the upgrade, since the provider changed), and if you run Octane or high-volume endpoints, warm the mapper once per worker. Enable whatever class-metadata cache the config offers; check it's actually on in production, because config:cache does not enable it for you.

Config caching quirk. Any package config published as a PHP file with closures or enum references will break php artisan config:cache. Run config:cache locally once before shipping to production; the failure mode is a boot-time fatal, not a graceful degrade.

Extension points worth knowing: the middleware stack is the sanctioned place for cross-cutting concerns (debug logging of mapped payloads, null-to-default handling, post-map validation). Custom converters/attributes are the escape hatch for one-off vendor weirdness — write one and register it rather than special-casing in twelve controllers.

Support and abandonment risk — improved, but not resolved. 3.0.0 is a genuine sign of life: Laravel 13 support, CI maintenance (actions/checkout v2 → v6 is someone actually reading Dependabot PRs), and added changelog/release automation. That is a materially better signal than the previous release cadence. Against it: zero dependents, a tiny star count, and release notes that contradict themselves about the supported framework range. Pin exactly ("json-mapper/laravel-package": "^3.0" with a committed lock), read the CHANGELOG and the diff on upgrade, and stay ready to drop to the upstream json-mapper/json-mapper directly — the wrapper is small enough that replacing its service provider with a five-line binding in your own AppServiceProvider is a half-hour job. Structure your code as if that day is coming: depend on JsonMapperInterface, not on the facade or the concrete wrapper class.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource