Wat je moet weten over JSON:API support in Laravel 13

Artikel
Björn Brala
8 april 2026

Laravel 13 is uit. Verborgen in de release notes, tussen de AI SDK en vector search, zit iets waar ik al jaren op wacht: JSON:API-ondersteuning. Ik onderhoud de JSON:API-module in Drupal Core, een implementatie die sinds 2019 miljarden productieaanvragen heeft verwerkt op duizenden websites. SWIS gebruikt JSON:API al jaren als standaard API dialect en onderhoudt daarom ook onze eigen PHP JSON:API client. Mijn eerste reactie was "yay" toen ik het pull request zag voor de ondersteuning op GitHub. Dit is niet alleen een mijlpaal voor de specificatie, maar ook een grote stap vooruit voorhet bouwen van API's.

Wat Laravel daadwerkelijk heeft toegevoegd

De feature is te vinden onder Illuminate\Http\Resources\JsonApi. Het startpunt is JsonApiResource, dat je genereert met:

php artisan make:resource PostResource --json-api

Dat levert dan het volgende op:

class PostResource extends JsonApiResource
{
    public $attributes = [
    // ...
    ];

    public $relationships = [
        // ...
    ];
}

Vul de configuratie en je bent al een heel eind. Een voorbeeld van een complete resource:

class PostResource extends JsonApiResource
{
    protected array $attributes = ['title', 'content'];

    protected array $relationships = [
        'author' => AuthorResource::class,
        'comments',
    ];
}

Strings in $attributes worden automatisch uit het model gelezen. Strings in $relationships worden automatisch vertaald naar de resource class van het gerelateerde model. Voor meer controle kun je toAttributes(Request $request) en toRelationships(Request $request) als methods overschrijven en je eigen code gebruiken.

De output voor een enkele resource:

{
  "data": {
    "id": "1",
    "type": "posts",
    "attributes": {
      "title": "Hello World",
      "content": "..."
    },
    "relationships": {
      "author": {
        "data": { "id": "4", "type": "authors" }
      }
    }
  },
  "included": [
    {
      "id": "4",
      "type": "authors",
      "attributes": { "name": "Bjorn Brala" }
    }
  ]
}

Met automatisch Content-Type: application/vnd.api+json op de response.

Dus wat automatiseert en valideert Laravel eigenlijk?

De data key is verplicht. Aanroepen van JsonApiResource::wrap() of JsonApiResource::withoutWrapping() gooien een BadMethodCallException. Maar goed, dat is wel vrij basic.

Type komt uit de Resource class, niet uit het model. Een AuthorResource die een User model gebruikt, produceert het type "authors", niet "users". De logica haalt Resource uit de naam van de class, zet het om naar snake case en maakt het meervoud. Dit is belangrijk: als je je resource AuthorResource noemt om users in een author-context te tonen, dan zien clients type authors. Dat is bewust, maar kan verwarrend zijn als je minder bekend bent met de specificatie.

Missende relaties worden automatisch geladen met loadMissing(). Als een client ?include=posts meestuurt, roept de resource automatisch $model->loadMissing(['posts']) aan. Dit is dus zonder eager loading en kan performance implicaties hebben.

Resources zonder Eloquent model werken ook, maar dan moet je zelf toId() en toType() toevoegen. Zonder die methodes geeft het framework een ResourceIdentificationException. De "magie" werkt dus vooral met Eloquent.


Waarom JSON:API belangrijk is

In 2019 gaf ik een talk op DrupalJam met de titel "Stop the noise!". De kern: JSON:API helpt om eindeloze discussies te voorkomen. Denk aan vragen zoals: nest je gerelateerde data of verwijs je ernaar? Gebruik je page, offset of cursor voor paginatie? Hoe noem je errors? JSON:API maakt een einde aan deze discussies. De specificatie bepaalt. Jij implementeert. Dat geldt in 2026 nog steeds. Maar de echte waarde zit in de structuur.

Het compound document is misschien wel de sterkste feature. Als een client ?include=author,comments meestuurt, krijg je in één response zowel de hoofdresource als de gerelateerde data. Zonder duplicatie, netjes gestructureerd en direct bruikbaar voor de client, zonder extra requests. Geen custom ?expand= parameter. Geen geneste JSON die je datamodel breekt. Een generieke client kan elke conforme server verwerken zonder iets van jouw domein te weten.

Sparse fieldsets zijn een tweede sterke feature waarmee je controle krijgt over de data in je response. Met ?fields[posts]=title,slug retourneert alleen die attributen. Als je mobiele client de volledige artikelinhoud niet nodig heeft in een lijstweergave, hoeft de mobiele client het niet te ontvangen. Ook hier geldt: geen custom endpoint, geen eigen parameters.

Met deze twee features verdwijnen grote discussies. "Moeten we een summary endpoint toevoegen?" Niet nodig. "Moeten we de author nesten of alleen een ID geven?" De specificatie regelt het.

We hebben swisnl/json-api-client gebouwd omdat je, zodra je een JSON:API-server hebt, ook een client wilt die het formaat generiek begrijpt. Dat is alleen mogelijk omdat de specificatie zo duidelijk en stabiel is.


Wat Drupal eerder al had opgelost

Drupal's JSON:API-module zit sinds 2019 in core. Zeven jaar productiegebruik. Als ik vergelijk wat Laravel heeft uitgebracht met wat Drupal biedt:

FeatureLaravelDrupal
Response structuur (data, attributes, relationships, included)
Sparse fieldsets (?fields[type]=...)
Compound documents (?include=...)
application/vnd.api+json Content-Type on success
JSON:API error format (errors[].source.pointer)
"compliant" pagination (page[number], page[size])partial¹
Relationship links (self, related in relationship objects)
Content negotiation (406 on invalid Accept parameters)
Filtering and sorting

¹ ?page=1 werkt; page[number]/page[size] vereist spatie/laravel-json-api-paginate. De specificatie schrijft paginatie niet strikt voor, dus technisch compliant.

 

Error format. Wanneer een validatiefout optreedt in Drupal, is de response:

{
  "errors": [
    {
      "status": "422",
      "title": "Unprocessable Entity",
      "source": { "pointer": "/data/attributes/title" },
      "detail": "The title field is required."
    }
  ]
}

Met Content-Type: application/vnd.api+json.

Wanneer een validatiefout optreedt in Laravel 13 op een JsonApiResource endpoint, is de response:

{
  "message": "The title field is required.",
  "errors": { "title": ["The title field is required."] }
}

Met Content-Type: application/json, wat vreemd is. De exception handler weet dus niets van JSON:API helaas.

Dit is belangrijk voor interoperabiliteit. Het error format uit de specificatie geeft clients source.pointer, een JSON Pointer naar het veld dat faalde. Clients die errors[].source.pointer parsen voor veldspecifieke fouten zullen crashen of niets tonen. Een goed gebouwde client detecteert de Content-Type mismatch en geeft een fout. Een minder zorgvuldige client negeert dit.

Spec-"compliant" paginatie. Laravel's paginator produceert ?page=1. Uit de tests:

{
  "links": {
    "first": "http://example.com/posts?page=1",
    "last": "http://example.com/posts?page=1"
  }
}

De JSON:API-specificatie raadt aan om de page keys te gebruiken: page[number], page[size], page[offset], als een conventie. spatie/laravel-json-api-paginate lost dit op met een ->jsonPaginate() methode die deze keys gebruikt en de juiste links genereert. Het is een eenvoudige aanpassing, maar page= is ook prima, de specificatie verplicht geen formaat.

Relationship links Drupal genereert self en related links in relationship objects, zoals de specificatie aanbeveelt:

{
  "relationships": {
    "author": {
      "links": {
        "self": "/posts/1/relationships/author",
        "related": "/posts/1/author"
      },
      "data": {
        "id": "4",
        "type": "users"
      }
    }
  }
}

Met links kunnen clients relaties lazy-loaden zonder een volledige compound document request. Laravel laat ze volledig weg.

Content negotiation De specificatie vereist dat servers 406 Not Acceptable teruggeven wanneer een client Accept: application/vnd.api+json met parameters stuurt. Laravel controleert dit niet. In de praktijk meestal geen probleem, maar het hoort bij "spec compliant".

Filtering en sorting Drupal implementeert filtering en sorting volgens de aanbevolen formats. Laravel laat dit volledig over aan applicatiecode. spatie/laravel-query-builder of laravel-json-api/laravel maken dit volledig mogelijk in Laravel.


Het timacdonald/json-api experiment

Als de API van JsonApiResource je bekend voorkomt, klopt dat. Tim MacDonald's package timacdonald/json-api bood al jaren een vergelijkbaar patroon met $attributes/$relationships patroon, lang voordat dit in Laravel zat. Zijn package liet zien dat het werkte. Laravel nam het over. Een mooi voorbeeld van open source.


Wat dit in de praktijk betekent

Onderhoud je zowel een Laravel API als de client zelf, dan zijn kleine verschillen met de specificatie vaak geen probleem.

Implementeer je eigen error middleware, gebruik spatie/laravel-json-api-paginate voor paginatie, spatie/laravel-query-builder voor filtering, sla relationship links over. Zo heb je snel een werkbare JSON:API met minimale moeite.

Maar bouw je voor andere systemen, zoals een JavaScript frontend met JSON:API library, een mobiele app of een andere consumer, dan moet je dichter naar de specificaties toe.

  1. Voeg JSON:API error formatting toe in je exception handler
  2. Gebruik ->jsonPaginate() van spatie/laravel-json-api-paginate
  3. Bepaal of je relationship links nodig hebt (en voeg ze toe via toLinks())

Dit error middleware kost niet veel tijd, maar je moet wel weten dát het nodig is. En daarvoor moet je de specificatie kennen.

Het grootste risico is dat teams JsonApiResource gebruiken, hun API uitbrengen en het "JSON:API compliant" noemen in hun documentatie. Helaas zal niet elke client tevreden zijn door de gaten tussen de specificatie en de implementatie.


Dus wat vind ik ervan?

Laravel heeft een goede stap gezet. Ze hebben de output structuur en de twee belangrijkste client-features geïmplementeerd: sparse fieldsets en compound documents. Tegelijkertijd hebben ze bewust grenzen gesteld: filtering, sorting en content negotiation vallen er nog buiten. De implementatie is compact en de standaardinstellingen zijn goed, en dat is lastiger dan het lijkt.

Wat nog mist, is vooral het error format. Dat hoort niet bij de applicatielaag. Een server die application/vnd.api+json spreekt bij succes en application/json bij fouten, spreekt eigenlijk twee verschillende talen. Dat kan beter.

Paginatie en filtering zijn minder een implementatieprobleem en meer een documentatieprobleem. De docs zouden duidelijk moeten verwijzen naar spatie packages.

Het belangrijkste signaal is wel positief. JSON:API wint al jaren langzaam terrein. Drupal liet al zien dat het op grote schaal werkt. Nu ook het populairste PHP-framework, Lavarel, het ondersteunt, hebben teams naast REST en GraphQL een derde, duidelijke optie: een API gebaseerd op een vaste specificatie.

"Stop the noise", zei ik in 2019. Laravel is het daarmee eens te zijn.


Ik ga beginnen, wat nu?

  1. Begin met JsonApiResource, de basis werkt, de DX is goed
  2. Voeg spatie/laravel-json-api-paginate toe
  3. Lees de specificatie!

Sla het bekijken van Laravel JSON:API package niet over. Het is een volledige implementatie van de specificatie met extra features. Zo bouwen wij bij SWIS al jaren JSON:API servers in Laravel.

Wil je zien hoe een complete JSON:API-implementatie eruitziet in PHP, dan blijft Drupal een goed voorbeeld. Het draait al zeven jaar in productie, heeft volledige compliance en miljarden requests. Veel complexe problemen zijn al opgelost.

Björn Brala is de JSON:API maintainer voor Drupal Core en werkt bij SWIS, waar we swisnl/json-api-client onderhouden.

Björn Brala is de JSON:API maintainer voor Drupal Core en werkt bij SWIS, bouwers van swisnl/json-api-client. Neem contact op met via mail, Drupal's Slack of 071-576 13 23

Bjorn Brala - CTO, Drupal Core JSON:API maintainer