Specification | Implementation

@see jsonapi.org


Philipp Marien / @philippmarien | Paul Gütschow

Abstract


  1. JSON:API
    1. Introduction
    2. Implementations
  2. Libraries
    1. enm/json-api-common
    2. enm/json-api-server
  3. Symfony Bundles
    1. enm/json-api-server-bundle

Introduction


JSON API is a specification for how a client should request that resources be fetched or modified, and how a server should respond to those requests.
http://jsonapi.org/format/#introduction

2013-05-03 ⇛ Initial release

2015-05-29 ⇛ 1.0 final release

REST over HTTP

Content-Type: application/vnd.api+json

Documents

  • Resource(s) ⇛ "data"
  • Meta Information ⇛ "meta"
  • Errors ⇛ "errors"
  • JSON API ⇛ "jsonapi"
  • Links ⇛ "links"
  • Related Resources ⇛ "included"

Example of a full Document (Top-Level)


{
    "links": { ... },
    "data": [ ... ],
    "included": [ ... ],
    "errors": [ ... ],
    "meta": { ... },
    "jsonapi": {
        "version": "1.0"
    }
}

Resources


like Entities, Identified by URI's

  • Identifier:
    • Type
    • Id (UUID recommended)
  • Attributes
  • Relationships
  • Links
  • Meta Information

Document Structure


Resource Objects ("data")


{
    "type": "talks",
    "id": "c9069d5c-1205-4043-9b8a-cb833f7235b5",
    "attributes": {
        "title": "JSON API"
    },
    "relationships": { ... }, //optional
    "links": { ... }, //optional
    "meta": { ... } //optional
}

Resource Identifier Objects

{
    "type": "talks",
    "id": "c9069d5c-1205-4043-9b8a-cb833f7235b5",
    "meta": { ... } // optional
}

Compound Documents ("included")


{
    "data": { ... },
    "included": [
        {
            "type": "persons",
            "id": "69d447ed-a9a4-41f8-8904-bfc597a0e2b9",
            "attributes": {
                "firstName": "Philipp",
                "lastName": "Marien"
            },
            "relationships": { ... }
        }
    ]
}

Meta Information ("meta")


"meta": {
    "createdAt": "2017-06-06T09:30:00+02:00"
}

Links ("links")


"links": {
    "self": "http://example.com/talks/c9069d5c..."
}
"links": {
    "authors": {
        "href": "http://example.com/talks/c9069d5c.../authors",
        "meta": {
            "count": 1
        }
    }
}

Errors ("errors")


"errors": [
    {
        "id": "e1e468ec-4762-47e3-ad5d-8651d39b6082",
        "links": {
            "about": "http/example.com/errors/e1e468ec-4762-47e3-ad5d-8651d39b6082"
        },
        "status": "400",
        "code": "1",
        "title": "Invalid Request",
        "detail": "Filter \"title\" can not be applied on post request!",
        "source": {
            "pointer": "/data",
            "parameter": "filter[title]"
        },
        "meta": {
            "trace": "..."
        }
    }
]

Relationships


"relationships": {
    "slides": {
        "links": {
            "self":  "http://example.com/talks/c9069d5c-1205-4043-9b8a-cb833f7235b5/relationships/slides",
            "related":  "http://example.com/talks/c9069d5c-1205-4043-9b8a-cb833f7235b5/slides"
        },
        "data": [
            {
                "type": "slides",
                "id": "964179f2-cea8-49a6-ba07-58083111adb7",
                "meta": {
                    "page": 1
                }
            }
        ],
        "meta": {
            "count": 1
        }
    },
    "authors": {
        "data": [
            {
                "type": "persons",
                "id": "69d447ed-a9a4-41f8-8904-bfc597a0e2b9"
            }
        ]
    }
}

Endpoints


Fetch - Create -Patch - Delete

Fetch Resource

GET /{type}/{id}
GET /talks/c9069d5c-1205-4043-9b8a-cb833f7235b5

{
    "data": {
        "type": "talks",
        "id": "c9069d5c-1205-4043-9b8a-cb833f7235b5",
        "attributes": {
            "title": "JSON API"
        },
        "relationships": { ... },
        "links": { ... },
        "meta": {
            "createdAt": "2017-06-06T09:30:00+02:00"
        }
    }
}

Fetch Resources


GET /{type}
GET /talks

{
    "data": [
        { ... },
        { ... }
    ]
}

Fetch Relationships


GET /{type}/{id}/relationship/{relationship}
GET /talks/c9069d5c-1205-4043-9b8a-cb833f7235b5/relationship/authors

{
    "data": [
        {
            "type": "persons",
            "id": "69d447ed-a9a4-41f8-8904-bfc597a0e2b9"
        }
    ]
}

Fetch Related Resources


GET /{type}/{id}/{relationship}
GET /talks/c9069d5c-1205-4043-9b8a-cb833f7235b5/authors

{
    "data": [
        {
            "type": "persons",
            "id": "69d447ed-a9a4-41f8-8904-bfc597a0e2b9",
            "attributes": {
                "firstName": "Philipp",
                "lastName": "Marien"
            }
        }
    ]
}

Create Resource


POST /{type}
POST /talks

{
    "data": {
        "type": "talks",
        // optional UUID, generated by server if not provided
        "id": "f3c5cb5c-bd34-4727-ad39-1110b99cfcb9",
        "attributes": {
            "title": "JSON API PHP Library"
        }
    }
}

Patch Resource


PATCH /{type}/{id}
PATCH /talks/c9069d5c-1205-4043-9b8a-cb833f7235b5

{
    "data": {
        "type": "talks",
        "id": "c9069d5c-1205-4043-9b8a-cb833f7235b5",
        "attributes": {
            "title": "JSON API - Slides"
        }
    }
}

Delete Resource


DELETE /{type}/{id}
  • 202 Accepted
  • 204 No Content
  • 200 OK (Document with top-level meta data)
  • 404 NOT FOUND

Fetching Data

Inclusion of Related Resources


GET /talks?include=authors
GET /talks?include=authors,slides
GET /talks/c9069d5c-1205-4043-9b8a-cb833f7235b5?include=slides
GET /talks?include=authors.talks
GET /talks?include=authors,authors.talks

Sparse Fieldsets


GET /persons?fields[persons]=firstName
GET /persons?fields[persons]=firstName,lastName
GET /persons?include=talks&fields[persons]=firstName,lastName&fields[talks]=title

Sorting


GET /talks?sort=title
GET /persons?sort=lastName,firstName
GET /talks?sort=-title
GET /talks?sort=authors.lastName

Pagination


GET /talks?page[offset]=0&page[limit]=10
GET /talks?page[number]=1&page[size]=10
{
    "links": {
        "self": "http://example.com/talks?page[offset]=9&page[limit]=10",
        "first": "http://example.com/talks",
        "last": "http://example.com/talks?page[offset]=29&page[limit]=10",
        "prev": "http://example.com/talks?page[offset]=0&page[limit]=10",
        "next": "http://example.com/talks?page[offset]=19&page[limit]=10"
    },
    "data": { ... }
}

Filtering


GET /talks?filter[title]=JSON
GET /talks?filter[author][firstName]=Philipp

Kurze Pause.

PHP Library (>= 7.0)


composer require enm/json-api-common

Json Api


  • generateUuid
  • resource
  • singleResourceDocument
  • multiResourceDocument
  • serializeDocument
  • deserializeDocument
  • toOneRelationship
  • toManyRelationship

Document


  • shouldBeHandledAsCollection
  • links
  • data
  • included
  • metaInformation
  • errors
  • jsonApi
  • httpStatus
  • withHttpStatus

Resource


  • type
  • id
  • attributes
  • relationships
  • links
  • metaInformation
  • duplicate

Relationship


  • shouldBeHandledAsCollection
  • name
  • related
  • links
  • metaInformation
  • duplicate

Key-Value-Collection


  • all
  • count
  • isEmpty
  • has
  • getRequired
  • getOptional
  • createSubCollection
  • merge
  • mergeCollection
  • set
  • remove
composer require enm/json-api-server

Request Handler


  • fetchResource
  • fetchResources
  • fetchRelationship
  • saveResource
  • deleteResource
  • modifyRelationship

Resource Provider


  • findResource
  • findResources
  • createResource
  • patchResource
  • deleteResource
  • modifyRelationship

Usage


<?php declare(strict_types=1);
require __DIR__.'/vendor/autoload.php';

$requestHandler = new \Enm\JsonApi\Server\RequestHandler\RequestHandlerRegistry();
$requestHandler->addRequestHandler('talks', new TalkRequestHandler());

$jsonApi = new \Enm\JsonApi\Server\JsonApiServer($requestHandler, '/api');

$request = new \GuzzleHttp\Psr7\Request(
    $_SERVER['REQUEST_METHOD'],
    $_SERVER['REQUEST_URI'],
    getallheaders(),
    file_get_contents('php://input')
);

\Http\Response\send($jsonApi->handleHttpRequest($request));

Json Api Aware


  • JsonApiAwareInterface
  • JsonApiAwareTrait
$this->jsonApi();

Advanced Usage


\Enm\JsonApi\Server\RequestHandler
  • FetchOnlyTrait
  • NoRelationshipsTrait
  • SeparatedSaveTrait
  • SeparatedRelationshipSaveTrait
use \Enm\JsonApi\Server\Pagination\PaginationTrait;

$this->setPaginationLinkGenerator(
    new \Enm\JsonApi\Server\Pagination\OffsetPaginationLinkGenerator()
);

$this->paginate($document, $request, $resultCount);

Symfony Integration (~2.7|~3.0)


composer require enm/json-api-server-bundle

Configuration

enm_json_api_server:
    debug: false
    api_prefix: "/api"
    logger: "logger"
    psr7_factory: "your_psr7_factory_service"
    http_foundation_factory: "your_http_foundation_factory_service"
    pagination:
        limit: 10

Routing

json_api:
  resource: "@EnmJsonApiServerBundle/Resources/config/routing.xml" 

Request Handler


AppBundle\RequestHandler\TalkRequestHandler:
    tags:
      - { name: 'json_api_server.request_handler', type: 'talks' }

AppBundle\RequestHandler\YourGenericRequestHandler:
    tags:
      - { name: 'json_api_server.request_handler' }

Resource Provider


AppBundle\ResourceProvider\TalkProvider:
    tags:
      - { name: 'json_api_server.resource_provider', type: 'talks' }

Live Demo.

Fragen?

Vielen Dank!