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

Json Request Bundle Laravel Package

flying-narwhal/json-request-bundle

Symfony bundle that decodes JSON request bodies into Symfony's request parameter bag, letting controllers read JSON payloads through the usual $request->request API without custom parsing.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small Symfony bundle whose job is narrow: detect JSON requests and populate Symfony's $request->request parameter bag with the decoded payload, so controllers can use $request->request->get('field') instead of calling json_decode($request->getContent(), true) by hand. It effectively backfills $_POST for application/json bodies (including PUT/PATCH/DELETE, where PHP never populates $_POST).

Minimal steps:

  1. composer require flying-narwhal/json-request-bundle
  2. Register it in config/bundles.php (FlyingNarwhal\JsonRequestBundle\JsonRequestBundle::class => ['all' => true]) if Flex doesn't do it for you.
  3. Check composer.json in the vendor directory first — confirm the symfony/http-kernel constraint matches your Symfony version before going further. With 6 stars and 0 dependents, version compatibility is the most likely first blocker.

Where to look first, in order:

  • README.md — the usage contract, if it's kept current.
  • src/EventListener/ (or src/RequestTransformer/) — the kernel.request listener that does the decoding. This is ~15–40 lines and is the entire behavior of the package; read it.
  • src/DependencyInjection/Configuration.php — the exact config keys (global on/off, content-type matching, error behavior). Don't guess these from the README.
  • composer.json — PHP version, Symfony constraints, whether tests exist.

First use case: a POST /api/orders endpoint accepting a JSON body. Before the bundle you'd write $data = json_decode($request->getContent(), true) ?? throw new BadRequestHttpException(); at the top of every action. After, the controller reads $request->request->get('customer_id') and stays in Symfony idiom (including $request->request->all() for mass-assignment-style hydration into a DTO).

Before adopting, note that Symfony 6.3+ ships #[MapRequestPayload] and the RequestPayloadValueResolver natively. If you're on a modern Symfony, this bundle's value is mostly historical. On 5.4/6.0–6.2 it fills a real gap cheaply.

Implementation Patterns

Global vs. route-scoped. Most such bundles apply to any request whose Content-Type matches. Confirm whether the listener checks the method, the content type, or both; if it's global, decide deliberately whether you want JSON decoding on HTML form routes too (usually harmless, occasionally surprising). If the bundle exposes an enabled/match config, prefer narrowing it over maintaining a fork.

Controller reads from the request bag, not getContent(). Pick one convention per codebase and stick to it. Mixing $request->request->get() and $request->getContent() in the same project is the fastest way to get inconsistent behavior when someone later disables the bundle or adds a route without it.

public function create(Request $request, ValidatorInterface $validator): Response
{
    $dto = new CreateOrderDto();
    $dto->customerId = $request->request->get('customer_id');
    // ... hydrate remaining fields

    $errors = $validator->validate($dto);
    if (count($errors) > 0) {
        return $this->json(['errors' => (string) $errors], 422);
    }

    // ...
}

Hydration via serializer. A clean pattern is to let the bundle get you as far as $request->request->all(), then hand that array to Serializer::denormalize($payload, CreateOrderDto::class, 'json'). This keeps validation and type coercion in one place and is a one-line swap to #[MapRequestPayload] later.

Error handling. Decide early what a malformed body returns. If the listener throws BadRequestHttpException, Symfony renders 400 automatically — good. If it swallows errors and leaves the bag empty, you'll get confusing 422s downstream instead of a clear 400. Add a kernel.exception listener or a problem+json response format so API clients get a consistent shape.

Symfony Forms and ParamConverter. The bundle only fills $request->request; forms still need to be submitted with $form->submit($request->request->all(), false) — and note the clearMissing = false flag, since with JSON you typically want PATCH semantics where absent keys don't null out fields. ParamConverter/EntityValueResolver route-parameter resolution is unaffected.

Subrequests and ESI. The listener fires on kernel.request for subrequests too. If your JSON endpoints are ever invoked as fragments, you can get double-decoding or unexpected 400s. Gate on $request->isMainRequest() in a custom listener if this bites.

Testing. Exercise it the way real clients do — through the kernel, with the header set:

$client->request(
    'POST',
    '/api/orders',
    [], [], ['CONTENT_TYPE' => 'application/json'],
    json_encode(['customer_id' => 42])
);

Never call the action directly with Request::create() and a hand-built bag; that bypasses the listener and tests nothing about the bundle.

Integration with json_login. The security firewall reads the body itself via the same content-type check. Both can coexist, but if you hit "invalid credentials" with an obviously correct payload, check listener priority — the firewall's authenticator must still see the raw stream or a fully populated bag, depending on version.

Gotchas and Tips

  • Content-Type strictness. The most common failure is a client sending text/plain, application/json;charset=UTF-8 (usually fine), or application/vnd.api+json. Check the listener's matcher; vendor +json suffixes are frequently missed. Curl defaults to application/x-www-form-urlencoded, which silently skips the bundle.
  • Empty body. json_decode('') returns null, not an array. If the listener calls $request->request->replace(null) you'll get a TypeError; if it replaces with [] you'll get a valid-looking empty request. Test Content-Length: 0 explicitly.
  • Top-level JSON arrays. A body of [1,2,3] decodes to a list, which is not what ParameterBag wants. The bundle may throw or may mangle it. If your API accepts arrays, don't rely on the parameter bag — read getContent() directly for those endpoints.
  • No type preservation guarantees. replace() with the decoded array preserves ints and booleans, so $request->request->get('qty') can return an int — but $request->query->get() never will. Don't write code that depends on string-ness of JSON-sourced params.
  • Listener priority. If it runs after something that already consumed php://input (rare, but a custom authenticator or a body-signing listener can), you get an empty payload. Use bin/console debug:event-dispatcher kernel.request to see the actual order and adjust priority via a compiler pass or service override.
  • No size limits. The bundle won't stop a 50 MB payload; json_decode will just eat memory. Rely on client_max_body_size (nginx) or post_max_size at the edge — Symfony's Request::getContent() isn't bounded either.
  • Debugging. The profiler's Request panel shows the final request bag contents — the fastest way to confirm whether the listener fired. If it's empty and you're sure the content type is right, the listener likely isn't registered: bin/console debug:container --tag=kernel.event_listener.
  • Extension points. You can decorate the listener service or add a second, higher-priority listener for cross-cutting concerns (logging bad JSON, stripping unexpected keys before validation). Overriding the service definition in services.yaml is more robust than forking.
  • Maintenance risk is the real gotcha. Six stars and zero dependents means no ecosystem pressure to keep it compatible. Vet the last commit date and open issues before committing. The behavior is small enough to inline as a project-local listener in about twenty lines if you'd rather not carry the dependency — and on Symfony 6.3+ #[MapRequestPayload] makes the bundle redundant anyway. Only reach for it if you're pinned to an older Symfony and want the convenience for free.
  • Mass-assignment surface. Populating the request bag makes $request->request->all() a tempting wholesale DTO hydrator. Bind only known fields; JSON clients can send arbitrary keys, and an unfiltered denormalize into an entity is an exploit waiting to happen.
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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator