PHPackages                             stetodd/jsonapi-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. stetodd/jsonapi-bundle

ActiveSymfony-bundle[API Development](/categories/api)

stetodd/jsonapi-bundle
======================

JSON:API request mapping and response serialisation for Symfony, built on league/fractal

v0.2.0(2w ago)026↓50%MITPHPPHP &gt;=8.4

Since Jun 12Pushed 2w agoCompare

[ Source](https://github.com/stetodd/jsonapi-bundle)[ Packagist](https://packagist.org/packages/stetodd/jsonapi-bundle)[ RSS](/packages/stetodd-jsonapi-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (22)Versions (3)Used By (0)

stetodd/jsonapi-bundle
======================

[](#stetoddjsonapi-bundle)

JSON:API request mapping and response serialisation for Symfony, built on league/fractal.

Install
-------

[](#install)

```
composer require stetodd/jsonapi-bundle
```

Register in `config/bundles.php`:

```
Stetodd\JsonApiBundle\StetoddJsonApiBundle::class => ['all' => true],
```

Configure in `config/packages/stetodd_json_api.yaml`:

```
stetodd_json_api:
    base_url: 'https://api.example.com'
    # recursion_limit: 4
    # relationship_routes:
    #     self: 'api_{type}_relationship_{relationship}_self'
    #     related: 'api_{type}_relationship_{relationship}_related'
```

Responses
---------

[](#responses)

Inject `Stetodd\JsonApiBundle\Response\JsonApiResponder` into controllers:

```
return $this->responder->item($palette);
return $this->responder->collection(Palette::class, $palettes, $pagedResult);
```

Resources implement `Contract\IdentifiableResourceInterface` (`getId(): \Stringable|string`). Each resource gets a transformer extending `League\Fractal\TransformerAbstract` and implementing `Contract\ResourceTransformerInterface`; transformers are auto-registered via autoconfiguration — no tagging needed.

Paginated collections take any `Contract\PagedResultInterface`. A fresh Fractal manager is built per response, so no include state leaks between requests (worker-mode safe).

Query features
--------------

[](#query-features)

The JSON:API query families are parsed by `Request\Query\JsonApiQuery::fromRequest()` and honoured per request:

- **`?include=`** — include paths are validated against the root transformer's available + default includes; an unsupported path is a `400` (per spec). Valid paths are passed to Fractal's `parseIncludes`.
- **`?fields[type]=`** — sparse fieldsets, applied to the primary resource *and* included resources via Fractal's `parseFieldsets`. Per the spec, fieldsets restrict relationships too: relationship links are only emitted for relationships named in the type's fieldset.
- **`?sort=`** — declare a resource's sortable fields on its transformer:

    ```
    #[JsonApiSortable('createdAt', 'historicalDate')]
    class ArtifactTransformer extends TransformerAbstract implements ResourceTransformerInterface
    ```

    then inject `Request\Query\SortResolver` into the list action and translate the validated sorts into your ordering:

    ```
    foreach ($sortResolver->forType(Artifact::class) as $sort) {
        // $sort->field, $sort->descending ('-' prefix)
    }
    ```

    An undeclared sort field is a `400` (per spec). The mapping of field names to a concrete ordering stays in the application.
- **`?page[number]=` / `?page[size]=`** — parsed onto `JsonApiQuery->pageNumber/pageSize` for the application's pagination resolver; the responder's pagination links emit the same shape (preserving `page[size]`).
- **`?filter[...]=`** — deliberately not interpreted by the bundle: filter strategy is application-defined. Convention: a list-query DTO with a `filter` property mapped via Symfony's `#[MapQueryString]`, so `?filter[x]=` nests naturally.

Auto-registered relationship endpoints
--------------------------------------

[](#auto-registered-relationship-endpoints)

Declare a resource's URL prefix and relationships on its transformer:

```
#[JsonApiResource(path: '/artifacts')]
#[JsonApiRelationship('fileUpload')]
#[JsonApiRelationship('detectedObjects', toMany: true)]
class ArtifactTransformer extends TransformerAbstract implements ResourceTransformerInterface
```

then import the route loader (before your attribute controllers, so a hand-written route with the same name — e.g. a POST relationship update — wins):

```
api_relationship_endpoints:
    resource: .
    type: stetodd_jsonapi_relationships
```

Each declaration registers `GET {path}/{id}/relationships/{segment}` (resource linkage) and `GET {path}/{id}/{segment}` (full related resource), named to the configured `relationship_routes` patterns — the same ones the serializer builds relationship links from, so links and endpoints stay in lockstep. The relationship data is fetched through the transformer's own Fractal `include{Name}()` method; the URL segment is the kebab-cased name (`fileUpload` → `file-upload`, overridable via `pathSegment:`).

The application provides one service: an implementation of `Contract\RelationshipSourceResolverInterface`, which loads the parent resource by id and enforces access control (return `null` → 404; throw an `AccessDeniedException` → 403). Alias the interface to your implementation:

```
Stetodd\JsonApiBundle\Contract\RelationshipSourceResolverInterface:
    alias: App\Ports\Api\Security\RelationshipSourceResolver
```

Error objects
-------------

[](#error-objects)

HttpExceptions raised on JSON:API routes are rendered as spec error objects (`application/vnd.api+json`):

```
{"errors": [{"status": "422", "title": "Unprocessable Entity",
             "detail": "This value should not be blank.",
             "source": {"pointer": "/data/attributes/title"}}]}
```

Validation failures (a `ValidationFailedException` as the exception's `previous`, which the mapping attributes and `#[MapQueryString]`/`#[MapRequestPayload]` all produce) yield one error per violation, with `source.parameter` for query input on safe methods and `source.pointer` for body input. Non-HTTP throwables (genuine 500s) keep Symfony's default handling, so debug traces survive in dev.

Content negotiation
-------------------

[](#content-negotiation)

Per the spec's server responsibilities, requests on JSON:API routes are rejected when they misuse the JSON:API media type itself: a `Content-Type: application/vnd.api+json` modified with media type parameters is a `415`, and an `Accept` header where **every** instance of the JSON:API media type carries parameters is a `406` (`q` is ignored). Other media types — e.g. plain `application/json` — pass through untouched. Both rejections render as JSON:API error objects.

Which routes count as JSON:API (for error rendering and content negotiation) is decided by route-name prefix:

```
stetodd_json_api:
    # route_name_prefixes: ['api_']
    errors:
        # enabled: true
    content_negotiation:
        # enabled: true
```

Conformance assertions
----------------------

[](#conformance-assertions)

`Test\JsonApiAssertionsTrait` (requires phpunit in your dev dependencies) provides spec-shape assertions for use in any test — one conformance test per surface catches structural regressions cheaply:

```
use Stetodd\JsonApiBundle\Test\JsonApiAssertionsTrait;

self::assertJsonApiDocument($decoded);          // top-level document rules
self::assertJsonApiResourceObject($decoded['data'], 'artifact');
self::assertJsonApiResourceLinkage($decoded['data']);  // relationship endpoints
self::assertJsonApiErrorDocument($decoded);     // errors[] shape incl. source
self::assertJsonApiPaginationLinks($decoded);   // self/first/last(/next/prev)
```

Resource self links
-------------------

[](#resource-self-links)

`#[JsonApiResource(path: …)]` also drives each resource's `links.self`: the serializer emits `{base_url}{path}/{id}` (e.g. `/geospatial/locations/{id}`) instead of Fractal's naive `{base_url}/{type}/{id}`, so transformers don't build self links by hand. A `links` key returned from `transform()` still wins — use it for resources with non-standard URLs (e.g. `/profile/me`).

Requests
--------

[](#requests)

Map JSON:API payloads onto DTOs with controller argument attributes:

```
public function create(
    #[MapAttributePayload(resourceType: Palette::class)]
    CreatePaletteAttributesRequest $attributes,
): JsonResponse {
```

Attribute DTOs implement `Request\AttributesDTOInterface` (use `AttributesDTOTrait`), relationship DTOs implement `Request\RelationshipsDTOInterface`. When a controller maps both, they are bound together through `Request\Context` / `ContextBindingInterface` (use `ContextBindingTrait`). Payloads are validated with symfony/validator; failures return 422 with violation messages.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance96

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~27 days

Total

2

Last Release

18d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1611641?v=4)[Steve Todd](/maintainers/stetodd)[@stetodd](https://github.com/stetodd)

---

Top Contributors

[![stetodd](https://avatars.githubusercontent.com/u/1611641?v=4)](https://github.com/stetodd "stetodd (6 commits)")

### Embed Badge

![Health badge](/badges/stetodd-jsonapi-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/stetodd-jsonapi-bundle/health.svg)](https://phpackages.com/packages/stetodd-jsonapi-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M401](/packages/easycorp-easyadmin-bundle)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M605](/packages/shopware-core)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M754](/packages/sylius-sylius)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6942.5M425](/packages/drupal-core-recommended)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9421.6k64](/packages/open-dxp-opendxp)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
