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

Dingo Serializer Switch Laravel Package

liyu/dingo-serializer-switch

Per-request serializer switching for Dingo API in Laravel. Clients choose array, JSON, or custom serializers via header or query parameter, letting one API serve multiple response formats without config changes or extra controllers.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin middleware that swaps the active Dingo API serializer for a single request. It does not replace Dingo's transformer; it just calls setSerializer() on the resolved Fractal adapter before your controller runs, and (in most setups) the framework resets it on the next request. Treat it as a request-scoped configuration toggle, not a full content-negotiation framework.

Minimal steps:

  1. Install via Composer, alongside a working dingo/api install (the middleware is useless without Dingo's transformer bound):
    composer require liyu/dingo-serializer-switch
    
  2. Read the README for the exact middleware class and config key names — the package ships a small config file (something like config/dingo-serializer-switch.php) and the middleware class lives under Liyu\DingoSerializerSwitch\.... Publish config if offered:
    php artisan vendor:publish --provider="Liyu\DingoSerializerSwitch\ServiceProvider"
    
  3. Register the middleware — either globally in app/Http/Kernel.php or, more commonly, as route middleware on your Dingle API route group:
    // app/Http/Kernel.php
    protected $routeMiddleware = [
        // ...
        'api.serializer' => \Liyu\DingoSerializerSwitch\...::class,
    ];
    
    Then attach it where you already use api.group / api.auth:
    $api->group(['middleware' => ['api.serializer']], function ($api) {
        $api->get('users', 'UserController@index');
    });
    
  4. Baseline config — set your "normal" serializer in config/api.php (defaults.serializer) so the default stays stable. The middleware only overrides it when the switch condition triggers.
  5. First use case — verify with a query string or Accept header, depending on how the package is configured:
    # default serializer
    curl https://api.example.com/users
    
    # switched serializer (example: JSON API)
    curl -H 'Accept: application/vnd.api+json' https://api.example.com/users
    
    Compare the JSON shapes — you should see {"data": {...}} vs {"data": [{"id": "1", "type": "users", "attributes": {...}}]} style differences. If nothing changes, the middleware isn't in the stack or the config mapping is wrong.

Implementation Patterns

Attach per route group, not globally. Serializer switching is a client-facing contract; attaching it to all routes (including internal health checks and webhooks) creates noise. Use it on the API group(s) where multiple client shapes are actually supported.

Config-driven mapping. The package is designed around a name → serializer class map in its config, typically something like:

// config/dingo-serializer-switch.php
return [
    'default' => 'array',
    'serializers' => [
        'array'   => League\Fractal\Serializer\ArraySerializer::class,
        'data'    => League\Fractal\Serializer\DataArraySerializer::class,
        'jsonapi' => League\Fractal\Serializer\JsonApiSerializer::class,
    ],
];

Keep the map explicit and never let clients pass an arbitrary FQCN — that would be an instantiation gadget.

Content negotiation via Accept. For a public API, map Accept media types to serializer names rather than exposing a query parameter:

  • application/json → DataArraySerializer (Dingo's typical default)
  • application/vnd.api+json → JsonApiSerializer
  • text/plain or a legacy client header → ArraySerializer

This lets older clients keep their data-unwrapped shape while new ones migrate to JSON:API without forking the API.

Query-string override for debugging and tests only. A ?serializer=jsonapi toggle is invaluable for QA and integration tests, but gate it behind app()->environment('local', 'testing') in your middleware wrapper, otherwise you've shipped an unauthenticated format switch to production.

Compose with Dingo versioning. Dingo's Accept-Version header already lives in the same header family. If you also do version negotiation, parse both in one place (a custom middleware wrapping this one) to avoid conflicting Accept parsing. Otherwise you'll get a v2 route returning a v1-shaped body because the serializer was chosen from the version string.

Testing pattern. Assert response shape per serializer, not just status codes:

$this->getJson('/users', ['Accept' => 'application/vnd.api+json'])
     ->assertJsonStructure(['data' => [['type', 'id', 'attributes']]]);

$this->getJson('/users')
     ->assertJsonStructure(['data' => [['id', 'name']]]);

Run the whole matrix in CI; serializer regressions are silent and only surface in client bugs.

Cache and ETag awareness. Serializer choice changes the byte-for-byte body, so any ETag computed over the response must include the serializer name. If you use api.etag, add the serializer to the cache key, or different clients with different Accept headers will get each other's cached payloads.

One middleware wrapper of your own. In practice you'll want a small local middleware that (a) resolves the target serializer, (b) calls app('Dingo\Api\Transformer\Adapter\Fractal')->setSerializer(new $class), and (c) restores the original in a finally. The package covers (b); a wrapper gives you (a), (c), and logging of which serializer each request chose — invaluable when a client says "the shape changed."

Gotchas and Tips

  • State leaks under Octane / Swoole. Dingo's Fractal adapter is a singleton. setSerializer() mutates shared state. In a classic FPM setup the app is torn down per request, but under Octane the worker is reused — you must reset the serializer after the response (in middleware's terminate() or a finally) or every subsequent request inherits the override. This is the single most important operational pitfall for this package.
  • Middleware ordering matters. Attach it after authentication/version middleware but before anything that touches the transformer (ETag hashing, response caching, custom error handlers). If a controller resolves the transformer on its own (e.g. $this->response->item(...)), the serializer must already be set.
  • Error responses are not transformed by this. Dingo serializes exceptions through errorFormat in config/api.php, not through the Fractal adapter. Swinging to JsonApiSerializer will not change error envelopes; clients expecting a JSON:API errors array will still receive {"message": "...", "status_code": 404}. If you need consistent error shapes, override errorFormat too, or normalize in a wrapper middleware.
  • Pagination and meta break across serializers. DataArraySerializer puts pagination under meta.pagination; JsonApiSerializer puts links under links and meta uses a different structure; ArraySerializer may drop pagination entirely from the top level. Clients that read meta.total will break under a different serializer. Document which serializers are supported per endpoint, and test paginated endpoints explicitly.
  • JsonApiSerializer requires resource type keys. Fractal needs setResourceName() (or a $resourceKey) on every transformer, including nested included resources. Missing names give you {"type": null, ...} and unpredictable URLs in links. Audit all your transformers before enabling it as a public option.
  • Config cache. php artisan config:cache will freeze your serializer map. If you hot-edit the config in dev and wonder why the switch does nothing, clear it: php artisan config:clear. In CI, always run with a cleared cache when running serializer matrix tests.
  • Nested/included relationships differ hugely. ?include=posts behaves differently under ArraySerializer (no links, no type) vs JsonApiSerializer (compound documents). Test includes with each serializer you expose, not just top-level resources.
  • Empty collections. ArraySerializer returns [] for empty, DataArraySerializer returns {"data": []}, NoopSerializer can return null. If a client does response.length, this is a breaking difference. Add a test for the empty case per serializer.
  • Don't expose a free-form serializer parameter. If you use the query-string mode, whitelist names in config. ?serializer=App\Evil\... must return 400, not instantiate.
  • Debugging tip. Log the negotiated serializer at the start of your custom wrapper:
    Log::debug('serializer', [
        'chosen' => $name,
        'accept' => $request->header('Accept'),
        'fractal' => get_class(app('Dingo\Api\Transformer\Adapter\Fractal')->getSerializer()),
    ]);
    
    Nine times out of ten a "wrong shape" bug is the middleware not being in the stack, or the Accept header being rewritten by a proxy/gateway in front of Laravel.
  • Extension point. If the package's built-in negotiation doesn't cover your case (e.g. you want tenant-based serializers), don't fork it — write your own tiny middleware that calls the same setSerializer() API and use the package only for its serializer resolution helper. The underlying Dingo call is stable and public; the package is a convenience, not a hard dependency.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings