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

Symfony Json Request Transformer Laravel Package

qandidate/symfony-json-request-transformer

Symfony package that decodes JSON request bodies into standard request parameters, so forms, validation, and controllers handle JSON APIs without custom parsing. A lightweight request transformer for cleaner Symfony JSON endpoints.

View on GitHub
Deep Wiki
Context7

Getting Started

This package registers a single kernel.request listener that decodes a JSON request body into Symfony's $request->request ParameterBag. The practical effect: a POST /orders with Content-Type: application/json and body {"sku":"ABC"} behaves, in your controller, exactly like a form-encoded request — $request->request->get('sku') returns "ABC".

Minimal steps

  1. Install:
    composer require qandidate/symfony-json-request-transformer
    
  2. Register the bundle (legacy apps use AppKernel::registerBundles(), Symfony 4+ uses config/bundles.php):
    // config/bundles.php
    Qandidate\Bundle\JsonRequestTransformerBundle\QandidateJsonRequestTransformerBundle::class => ['all' => true],
    
  3. Confirm the listener is wired onto kernel.request:
    bin/console debug:event-dispatcher kernel.request
    
    Look for the transformer listener and note its priority relative to Firewall, router, and any of your own kernel.request listeners.
  4. Send a request and inspect:
    curl -X POST https://app.local/orders \
         -H 'Content-Type: application/json' \
         -d '{"sku":"ABC","qty":2}'
    

Where to look first

  • The listener class itself (...\EventListener\JsonRequestTransformerListener::onKernelRequest) — it is ~20 lines and tells you the exact matching rule and failure behavior. Read it before trusting it in a security-sensitive path.
  • The bundle's service definition (Resources/config/services.yml or config/services.php) — this is where the tag and priority live.
  • Your firewall config in security.yaml — if you use JSON login or form login, the listener must run before the authenticator reads $request->request.

First use case

The canonical one is a JSON API endpoint that also feeds the Symfony Form component or Validator, so you can reuse $form->submit($request->request->all()) without writing a decoder in every controller. It is also immediately useful for PUT/PATCH requests, since Symfony's Request only auto-parses POST bodies.

⚠️ This package is archived (last release January 2023). Pin the version, vendor it, or plan the migration described below before building new surface area on it.

Implementation Patterns

Read from the parameter bag, not getContent()

public function create(Request $request): Response
{
    $payload = $request->request->all();   // already decoded
    // $request->getContent() still returns the raw JSON string if you need it
}

Feed Symfony Forms directly

The main reason to use this package instead of hand-rolled json_decode is that Forms, Validators, and the security authenticators all read $request->request. Once the bag is populated, the standard stack just works:

$form = $this->createForm(OrderType::class, new Order());
$form->submit($request->request->all());
if (!$form->isValid()) {
    return $this->json($form, 400);
}

DTO hydration without the Form component

Deserialize out of the bag rather than the raw body, so the listener remains the single decoding point:

$dto = $this->serializer->denormalize(
    $request->request->all(),
    CreateOrder::class
);

JSON login / security

Configure your authenticator (or json_login firewall) and ensure the transformer's kernel.request priority is higher than the firewall's. Verify with debug:event-dispatcher kernel.request. If you see the authenticator before the listener, credentials arrive empty and you get a generic "bad credentials" error with no obvious cause.

Defensive controller pattern

If the listener is optional in your stack (e.g. only registered in some environments), wrap the access:

$payload = $request->request->all();
if ($payload === [] && $request->getContent() !== '') {
    $payload = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
}

Testing

The listener keys off the Content-Type header. Symfony's test client will not set it for you:

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

Use CONTENT_TYPE (server var), not a headers array, in older Symfony versions. In Symfony 5.4+, the sixth argument is still the server array for KernelBrowser::request.

Content types

The match is a substring check for json in Content-Type, so application/json, application/json; charset=utf-8, and vendor types like application/vnd.api+json all pass through. Use that to your advantage for versioned APIs; be aware it also matches anything else containing the word "json" in the type string.

Migration target

On Symfony 6.3+ the built-in replacement is #[MapRequestPayload] on controller arguments (backed by symfony/serializer), or #[ValueResolver]-based custom resolvers. If you are on 6.3+ and writing new code, prefer that; keep this listener only where the Form component must receive the bag.

Gotchas and Tips

No Content-Type, no transformation. A JSON body sent without Content-Type: application/json leaves $request->request empty. This is the #1 support question — validate headers at your edge/proxy and fail fast with a 415.

Invalid JSON fails silently. The listener decodes and, on a syntax error, typically leaves the bag empty rather than throwing. Your controller then reports a validation error ("sku is required") instead of "malformed body". Add a kernel.request listener after the transformer that checks json_last_error() on the raw content, or a kernel.exception guard, so clients get a real 400 Bad Request.

Only arrays replace the bag. json_decode of a scalar ("hello", 42, true) or null will not populate parameters. If your API accepts bare JSON scalars, handle them explicitly with $request->getContent().

$request->request->replace() does not touch the query string. $request->query still holds the URL parameters; $request->get('x') checks attributes → query → request, so precedence surprises are possible when a query param and a body field share a name. Be explicit about which bag you read.

Numeric top-level arrays. A body of [{"a":1}] populates the bag with integer keys; $request->request->get('a') is null. This is expected but trips up people expecting ->all()[0]['a'].

Listener ordering matters more than the package docs suggest. Anything that reads $request->request on kernel.request before the transformer (custom listeners, rate limiters keyed on body fields, some authenticators) sees an empty bag. Audit with debug:event-dispatcher kernel.request and either raise the transformer's priority or lower your consumer's.

Empty body vs. empty object. {} decodes to an empty array and works fine; "" (no body at all) produces a JSON syntax error. If your endpoint allows an empty body, branch on $request->getContent() === '' before complaining.

The raw content survives. Calling json_decode($request->getContent(), true) again in a controller is harmless but wasteful and risks divergence if you ever change decoding flags (e.g. JSON_OBJECT_AS_ARRAY). Standardize on the bag or on a DTO resolver, not both.

Event class compatibility. Older releases register a listener typed against Symfony\Component\HttpKernel\Event\GetRequestEvent, which was removed in Symfony 5 in favor of RequestEvent. Check the listener signature against your Symfony version; if you are pinned to a release that still uses GetRequestEvent you will get a fatal error or a silently uninvoked listener. Since the project is archived, this is the most likely reason you will need to fork or patch it — the fix is a one-line change and a re-tag.

Debugging recipe. When requests look "empty", dump both sides:

dump($request->headers->get('Content-Type'), $request->getContent(), $request->request->all());

If getContent() has the JSON but request->all() is [], the listener is either not registered, ordered after your consumer, or short-circuiting on the content type.

Extension point. The class is small and not final in most releases. If you need to support multipart/form-data with a JSON part, or want to throw BadRequestHttpException on JSON_ERROR_*, subclass the listener, override onKernelRequest, and re-point the service id at your class — cheaper than forking the package.

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.
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
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