Resource Object Schemas

Introduction

Resource objects are the JSON API representations of your application's domain records (instances of PHP classes). Every PHP class that can appear in your JSON API documents must have a Schema class. This schema defines how to convert the PHP object into a JSON API resource object.

Credit where credit is due... the encoding of PHP objects to JSON API resources is handled by the neomerx/json-api package.

In previous versions we had an Eloquent schema. These are deprecated and will be removed for v2.0.0. We therefore recommend that you use generic schemas for any new schemas that you are creating. Documentation for this deprecated class can be found here.

Defining Resources

Your API's configuration contains a list of resources that appear in its JSON API documents in its resources array. This array maps the JSON API resource object type to the PHP class that it relates to. For example:

'resources' => [
    'posts' => App\Post::class,
    'comments' => App\Comment::class,
]

// ...

You need to list every resource that can appear in a JSON API document in the resources configuration, even resources that do not have API routes defined for them. This is so that the JSON API encoder can locate a schema for each PHP class it encounters.

Creating Schemas

To generate a schema that extends, use the following command. The generated schema will extend CloudCreativity\LaravelJsonApi\Schema\SchemaProvider.

php artisan make:json-api:schema <resource-type> [<api>]

If your use-eloquent option is set to true, the created schema will have some methods already filled. If you want to generate a non-Eloquent schema, add the -N option.

Identification

Every JSON API resource object must have a type and id member. To define the type, set the $resourceType property on your schema. The id is returned by the getId() method and the JSON API spec states that this must be a string.

If you have generated your schema, the class will already have the type property filled in plus the getId method implemented if it is for an Eloquent resource. For example:

class Schema extends SchemaProvider
{

    protected $resourceType = 'posts';

    /**
     * @param App\Post $resource
     * @return string
     */
    public function getId($resource)
    {
        return (string) $resource->getRouteKey();
    }
}

If you are using the Eloquent adapter and decide to use an id other than the model's route key, you must set the $primaryKey property on your adapter so that it matches your schema. For example, if your schema used $model->getKey() you would need to set the $primaryKey property on your adapter so that it matched the return value of $model->getKeyName().

Fields

A resource object’s attributes and its relationships are collectively called its “fields”.

Fields for a resource object must share a common namespace with each other and with type and id. In other words, a resource can not have an attribute and relationship with the same name, nor can it have an attribute or relationship named type or id.

Attributes

A resource object can contain an attributes object containing additional properties of the resource. Attributes are returned by the getAttributes() method on your schema. If you have generated your schema, this method will already be implemented.

As an example, a schema for a posts resource could look like this:

class Schema extends SchemaProvider
{

    // ...

    /**
     * @param App\Post $post
     * @return array
     */
    public function getAttributes($post)
    {
        return [
            'createdAt' => $post->created_at,
            'content' => $post->content,
            'publishedAt' => $post->published_at,
            'slug' => $post->slug,
            'title' => $post->title,
            'updatedAt' => $post->updated_at,
        ];
    }
}

The above schema would result in the following resource object:

{
    "type": "posts",
    "id": "1",
    "attributes": {
        "createdAt": "2018-01-01T11:12:13.356234Z",
        "content": "...",
        "publishedAt": "2018-01-01T12:30:10.258250Z",
        "slug": "my-first-post",
        "title": "My First Post",
        "updatedAt": "2018-01-01T12:30:10.258250Z"
    }
}

Relationships

A resource object may have a relationships key that holds a relationships object. This object describes linkages to other resource objects. Relationships can either be to-one or to-many. The JSON API spec allows these linkages to be described in resource relationships in multiple ways - either through a links, data or meta value, or a combination of all three.

It's worth mentioning again that every PHP class that could be returned as a related object must have a schema and be defined in your API's resources configuration. Otherwise you will get an error when encoding relationships.

A schema defines the relationships that are to be serialized for a resource object in its getRelationships() method.

A resource relationship can be described with URL links - either a self link or a related link. The self link provides the URI at which a client can obtain the relationship object. The related link provides the URI for obtaining the related resource object(s).

For example, if our App\Post model has a comments relationship, its relationship can be described as follows:

class Schema extends SchemaProvider
{
    $resourceType = 'posts';

    // ...

    public function getRelationships($post, $isPrimary, array $includeRelationships)
    {
        return [
            'comments' => [
                self::SHOW_SELF => true,
                self::SHOW_RELATED => true,
            ],
        ];
    }
}

This would generate the following resource object:

{
    "type": "posts",
    "id": "1",
    "relationships": {
        "comments": {
            "links": {
                "self": "/api/posts/1/relationships/comments",
                "related": "/api/posts/1/comments"
            }
        }
    }
}

These links will only work if you register them when define your API's routing. For related links, see Fetching Resources and for the self link, see Fetching Relationships.

Data

Relationship data contains the resource identifier for the related resource or resources. A resource identifier contains the unique combination of a type and id for the related resources. The JSON API encoder will create these resource identifiers when a PHP instance is returned for relationship data. For example:

class Schema extends SchemaProvider
{
    $resourceType = 'posts';

    // ...

    public function getRelationships($post, $isPrimary, array $includeRelationships)
    {
        return [
            'author' => [
                self::DATA => function () use ($post) {
                    return $post->author;
                },
            ],
        ];
    }
}

This would generate the following resource object:

{
    "type": "posts",
    "id": "1",
    "relationships": {
        "author": {
            "data": {
                "type": "users",
                "id": "123"
            }
        }
    }
}

Notice the return values are wrapped in closures. This is so that the cost of getting the related object is only incurred if the data is definitely going to be contained in the encoded JSON API response. For example, the data may be skipped if the client has requested sparse fieldsets.

If you need to variably include the data, you can use the SHOW_DATA option. The following example would only include the author data if the related resource was to be included in a compound document:

class Schema extends SchemaProvider
{
    protected $resourceType = 'posts';

    // ...

    public function getRelationships($post, $isPrimary, array $includeRelationships)
    {
        return [
            'author' => [
                self::SHOW_DATA => isset($includeRelationships['author']),
                self::DATA => function () use ($post) {
                    return $post->author;
                },
            ],
        ];
    }
}

We do not recommend using data for any to-many relationships that could have a large number of related resources. For example, if our posts resource has a comments relationship, it would not be sensible to return data for the related comments because a post could have hundreds of comments. This would result in very slow encoding speed. Large to-many relations are best represented using links.

Meta

A relationship object can contain a meta object, that can contain any information needed about the relationship. To return meta for a relationship on a resource object:

class Schema extends SchemaProvider
{
    protected $resourceType = 'posts';

    // ...

    public function getRelationships($post, $isPrimary, array $includeRelationships)
    {
        return [
            'comments' => [
                self::META => function () use ($post) {
                    return [
                        'total' => $post->comments_count,
                    ];
                },
            ],
        ];
    }
}

This would generate the following resource object:

{
    "type": "posts",
    "id": "1",
    "relationships": {
        "comments": {
            "meta": {
                "total": 3
            }
        }
    }
}

As with data, we wrap the meta in a closure so that the cost of generating it is only incurred if the relationship is definitely appearing in the encoded response. This however is optional - i.e. you would not need to wrap the meta in a closure if there is no cost involved in generating it.

A resource relationship can contain any combination of data, links and meta you want. For example, we use the following as our default relationship to give the client control over how it wants the relationship included:

class Schema extends SchemaProvider
{
    protected $resourceType = 'posts';

    // ...

    public function getRelationships($post, $isPrimary, array $includeRelationships)
    {
        return [
            'author' => [
                self::SHOW_SELF => true,
                self::SHOW_RELATED => true,
                self::SHOW_DATA => isset($includeRelationships['author']),
                self::DATA => function () use ($post) {
                    return $post->author;
                },
            ],
        ];
    }
}

Would generate the following resource object:

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

By default all resource objects will be encoded with their self link, e.g.:

{
    "type": "posts",
    "id": "1",
    "attributes": {
        "title": "My First Post",
        "content": "..."
    },
    "links": {
        "self": "/api/posts/1"
    }
}

You can change this behaviour by overloading the getResourceLinks or getIncludedResourceLinks methods. For example:

class Schema extends SchemaProvider
{
    // ...

    public function getResourceLinks($resource)
    {
        $links = parent::getResourceLinks($resource);
        $links['foo'] = $this->createLink('posts/foo');

        return $links;
    }

}

This would result in the following resource object:

{
    "type": "posts",
    "id": "1",
    "attributes": {
        "title": "My First Post",
        "content": "..."
    },
    "links": {
        "self": "/api/posts/1",
        "foo": "/api/posts/foo"
    }
}

The createLink method allows you to pass in link meta and set whether the URI is relative to the API or an absolute path.

If you want to only change the links when the resource is appearing in the included section of the JSON API document, overload the getIncludedResourceLinks() method instead.

Meta

You can add top-level meta to your resource object using the getPrimaryMeta() or getInclusionMeta() methods on your schema. These are called depending on whether your resource is appearing in either the primary data member of the JSON API document or the included member.

For example, the following would add meta to your resource object regardless of whether it is primary data or included in the document:

class Schema extends SchemaProvider
{
    // ...

    public function getPrimaryMeta($resource)
    {
        return ['foo' => 'bar'];
    }

    public function getInclusionMeta($resource)
    {
        return $this->getPrimaryMeta($resource);
    }

}

This would result in the following resource object:

{
    "type": "posts",
    "id": "1",
    "attributes": {
        "title": "My First Post",
        "content": "..."
    },
    "meta": {
        "foo": "bar"
    }
}