forestadmin-jsonapi-serializers 2.0.0.pre.beta.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +868 -0
- data/Rakefile +2 -0
- data/forestadmin-jsonapi-serializers.gemspec +28 -0
- data/lib/jsonapi-serializers.rb +13 -0
- data/lib/jsonapi-serializers/attributes.rb +79 -0
- data/lib/jsonapi-serializers/serializer.rb +589 -0
- data/lib/jsonapi-serializers/version.rb +7 -0
- data/spec/serializer_spec.rb +1337 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/factory.rb +44 -0
- data/spec/support/serializers.rb +264 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 356df271f9e8100577cd1942d1a511ae1bf6def1f315dc27054e9715decbaf1d
|
4
|
+
data.tar.gz: 81a470af825925fffc7f5df7d075fbd664ecab60c80add42225fc1c4663f993e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 38800d5ca9a6fe32a453ae1b29de17f2f1b8455b9d710919186356667ea921312368221535ae7eae503afb6a1056cd3206fdbf2f75ccf6f0dc9e8c4539ea7d29
|
7
|
+
data.tar.gz: 0120b3c6fd65b86d51d840aa39b1436e6120a11c4895a98d7d728e3c22567ca352f6fd9e5b15ca9f37fbbe8b3c0e7b324b3eb1226a4629e5629210d4cebdeedd
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Mike Fotinakis
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,868 @@
|
|
1
|
+
# JSONAPI::Serializers
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/fotinakis/jsonapi-serializers.svg?branch=master)](https://travis-ci.org/fotinakis/jsonapi-serializers)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/jsonapi-serializers.svg)](http://badge.fury.io/rb/jsonapi-serializers)
|
5
|
+
|
6
|
+
JSONAPI::Serializers is a simple library for serializing Ruby objects and their relationships into the [JSON:API format](http://jsonapi.org/format/).
|
7
|
+
|
8
|
+
This library is up-to-date with the finalized v1 JSON API spec.
|
9
|
+
|
10
|
+
* [Features](#features)
|
11
|
+
* [Installation](#installation)
|
12
|
+
* [Usage](#usage)
|
13
|
+
* [Define a serializer](#define-a-serializer)
|
14
|
+
* [Serialize an object](#serialize-an-object)
|
15
|
+
* [Serialize a collection](#serialize-a-collection)
|
16
|
+
* [Null handling](#null-handling)
|
17
|
+
* [Multiple attributes](#multiple-attributes)
|
18
|
+
* [Custom attributes](#custom-attributes)
|
19
|
+
* [More customizations](#more-customizations)
|
20
|
+
* [Base URL](#base-url)
|
21
|
+
* [Root metadata](#root-metadata)
|
22
|
+
* [Root links](#root-links)
|
23
|
+
* [Root errors](#root-errors)
|
24
|
+
* [Root jsonapi object](#root-jsonapi-object)
|
25
|
+
* [Explicit serializer discovery](#explicit-serializer-discovery)
|
26
|
+
* [Namespace serializers](#namespace-serializers)
|
27
|
+
* [Sparse fieldsets](#sparse-fieldsets)
|
28
|
+
* [Relationships](#relationships)
|
29
|
+
* [Compound documents and includes](#compound-documents-and-includes)
|
30
|
+
* [Relationship path handling](#relationship-path-handling)
|
31
|
+
* [Control links and data in relationships](#control-links-and-data-in-relationships)
|
32
|
+
* [Instrumentation](#instrumentation)
|
33
|
+
* [Rails example](#rails-example)
|
34
|
+
* [Sinatra example](#sinatra-example)
|
35
|
+
* [Unfinished business](#unfinished-business)
|
36
|
+
* [Contributing](#contributing)
|
37
|
+
|
38
|
+
## Features
|
39
|
+
|
40
|
+
* Works with **any Ruby web framework**, including Rails, Sinatra, etc. This is a pure Ruby library.
|
41
|
+
* Supports the readonly features of the JSON:API spec.
|
42
|
+
* **Full support for compound documents** ("side-loading") and the `include` parameter.
|
43
|
+
* Similar interface to ActiveModel::Serializers, should provide an easy migration path.
|
44
|
+
* Intentionally unopinionated and simple, allows you to structure your app however you would like and then serialize the objects at the end. Easy to integrate with your existing authorization systems and service objects.
|
45
|
+
|
46
|
+
JSONAPI::Serializers was built as an intentionally simple serialization interface. It makes no assumptions about your database structure or routes and it does not provide controllers or any create/update interface to the objects. It is a library, not a framework. You will probably still need to do work to make your API fully compliant with the nuances of the [JSON:API spec](http://jsonapi.org/format/), for things like supporting `/relationships` routes and for supporting write actions like creating or updating objects. If you are looking for a more complete and opinionated framework, see the [jsonapi-resources](https://github.com/cerebris/jsonapi-resources) project.
|
47
|
+
|
48
|
+
## Installation
|
49
|
+
|
50
|
+
Add this line to your application's Gemfile:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
gem 'jsonapi-serializers'
|
54
|
+
```
|
55
|
+
|
56
|
+
Or install directly with `gem install jsonapi-serializers`.
|
57
|
+
|
58
|
+
## Usage
|
59
|
+
|
60
|
+
### Define a serializer
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
require 'jsonapi-serializers'
|
64
|
+
|
65
|
+
class PostSerializer
|
66
|
+
include JSONAPI::Serializer
|
67
|
+
|
68
|
+
attribute :title
|
69
|
+
attribute :content
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
### Serialize an object
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
JSONAPI::Serializer.serialize(post)
|
77
|
+
```
|
78
|
+
|
79
|
+
Returns a hash:
|
80
|
+
```json
|
81
|
+
{
|
82
|
+
"data": {
|
83
|
+
"id": "1",
|
84
|
+
"type": "posts",
|
85
|
+
"attributes": {
|
86
|
+
"title": "Hello World",
|
87
|
+
"content": "Your first post"
|
88
|
+
},
|
89
|
+
"links": {
|
90
|
+
"self": "/posts/1"
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
```
|
95
|
+
|
96
|
+
### Serialize a collection
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
JSONAPI::Serializer.serialize(posts, is_collection: true)
|
100
|
+
```
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
|
104
|
+
```json
|
105
|
+
{
|
106
|
+
"data": [
|
107
|
+
{
|
108
|
+
"id": "1",
|
109
|
+
"type": "posts",
|
110
|
+
"attributes": {
|
111
|
+
"title": "Hello World",
|
112
|
+
"content": "Your first post"
|
113
|
+
},
|
114
|
+
"links": {
|
115
|
+
"self": "/posts/1"
|
116
|
+
}
|
117
|
+
},
|
118
|
+
{
|
119
|
+
"id": "2",
|
120
|
+
"type": "posts",
|
121
|
+
"attributes": {
|
122
|
+
"title": "Hello World again",
|
123
|
+
"content": "Your second post"
|
124
|
+
},
|
125
|
+
"links": {
|
126
|
+
"self": "/posts/2"
|
127
|
+
}
|
128
|
+
}
|
129
|
+
]
|
130
|
+
}
|
131
|
+
```
|
132
|
+
|
133
|
+
You must always pass `is_collection: true` when serializing a collection, see [Null handling](#null-handling).
|
134
|
+
|
135
|
+
### Null handling
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
JSONAPI::Serializer.serialize(nil)
|
139
|
+
```
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
```json
|
143
|
+
{
|
144
|
+
"data": null
|
145
|
+
}
|
146
|
+
```
|
147
|
+
|
148
|
+
And serializing an empty collection:
|
149
|
+
```ruby
|
150
|
+
JSONAPI::Serializer.serialize([], is_collection: true)
|
151
|
+
```
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
```json
|
155
|
+
{
|
156
|
+
"data": []
|
157
|
+
}
|
158
|
+
```
|
159
|
+
|
160
|
+
Note that the JSON:API spec distinguishes in how null/empty is handled for single objects vs. collections, so you must always provide `is_collection: true` when serializing multiple objects. If you attempt to serialize multiple objects without this flag (or a single object with it on) a `JSONAPI::Serializer::AmbiguousCollectionError` will be raised.
|
161
|
+
|
162
|
+
### Multiple attributes
|
163
|
+
You could declare multiple attributes at once:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
attributes :title, :body, :contents
|
167
|
+
```
|
168
|
+
|
169
|
+
### Custom attributes
|
170
|
+
|
171
|
+
By default the serializer looks for the same name of the attribute on the object it is given. You can customize this behavior by providing a block to `attribute`, `has_one`, or `has_many`:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
attribute :content do
|
175
|
+
object.body
|
176
|
+
end
|
177
|
+
|
178
|
+
has_one :comment do
|
179
|
+
Comment.where(post: object).take!
|
180
|
+
end
|
181
|
+
|
182
|
+
has_many :authors do
|
183
|
+
Author.where(post: object)
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
The block is evaluated within the serializer instance, so it has access to the `object` and `context` instance variables.
|
188
|
+
|
189
|
+
### More customizations
|
190
|
+
|
191
|
+
Many other formatting and customizations are possible by overriding any of the following instance methods on your serializers.
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
# Override this to customize the JSON:API "id" for this object.
|
195
|
+
# Always return a string from this method to conform with the JSON:API spec.
|
196
|
+
def id
|
197
|
+
object.slug.to_s
|
198
|
+
end
|
199
|
+
```
|
200
|
+
```ruby
|
201
|
+
# Override this to customize the JSON:API "type" for this object.
|
202
|
+
# By default, the type is the object's class name lowercased, pluralized, and dasherized,
|
203
|
+
# per the spec naming recommendations: http://jsonapi.org/recommendations/#naming
|
204
|
+
# For example, 'MyApp::LongCommment' will become the 'long-comments' type.
|
205
|
+
def type
|
206
|
+
'long-comments'
|
207
|
+
end
|
208
|
+
```
|
209
|
+
```ruby
|
210
|
+
# Override this to customize how attribute names are formatted.
|
211
|
+
# By default, attribute names are dasherized per the spec naming recommendations:
|
212
|
+
# http://jsonapi.org/recommendations/#naming
|
213
|
+
def format_name(attribute_name)
|
214
|
+
attribute_name.to_s.dasherize
|
215
|
+
end
|
216
|
+
```
|
217
|
+
```ruby
|
218
|
+
# The opposite of format_name. Override this if you override format_name.
|
219
|
+
def unformat_name(attribute_name)
|
220
|
+
attribute_name.to_s.underscore
|
221
|
+
end
|
222
|
+
```
|
223
|
+
```ruby
|
224
|
+
# Override this to provide resource-object metadata.
|
225
|
+
# http://jsonapi.org/format/#document-structure-resource-objects
|
226
|
+
def meta
|
227
|
+
end
|
228
|
+
```
|
229
|
+
```ruby
|
230
|
+
# Override this to set a base URL (http://example.com) for all links. No trailing slash.
|
231
|
+
def base_url
|
232
|
+
@base_url
|
233
|
+
end
|
234
|
+
```
|
235
|
+
```ruby
|
236
|
+
# Override this to provide a resource-object jsonapi object containing the version in use.
|
237
|
+
# http://jsonapi.org/format/#document-jsonapi-object
|
238
|
+
def jsonapi
|
239
|
+
end
|
240
|
+
```
|
241
|
+
```ruby
|
242
|
+
def self_link
|
243
|
+
"#{base_url}/#{type}/#{id}"
|
244
|
+
end
|
245
|
+
```
|
246
|
+
```ruby
|
247
|
+
def relationship_self_link(attribute_name)
|
248
|
+
"#{self_link}/relationships/#{format_name(attribute_name)}"
|
249
|
+
end
|
250
|
+
```
|
251
|
+
```ruby
|
252
|
+
def relationship_related_link(attribute_name)
|
253
|
+
"#{self_link}/#{format_name(attribute_name)}"
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
If you override `self_link`, `relationship_self_link`, or `relationship_related_link` to return `nil`, the link will be excluded from the serialized object.
|
258
|
+
|
259
|
+
### Base URL
|
260
|
+
|
261
|
+
You can override the `base_url` instance method to set a URL to be used in all links.
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
class BaseSerializer
|
265
|
+
include JSONAPI::Serializer
|
266
|
+
|
267
|
+
def base_url
|
268
|
+
'http://example.com'
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
class PostSerializer < BaseSerializer
|
273
|
+
attribute :title
|
274
|
+
attribute :content
|
275
|
+
|
276
|
+
has_one :author
|
277
|
+
has_many :comments
|
278
|
+
end
|
279
|
+
|
280
|
+
JSONAPI::Serializer.serialize(post)
|
281
|
+
```
|
282
|
+
|
283
|
+
Returns:
|
284
|
+
|
285
|
+
```json
|
286
|
+
{
|
287
|
+
"data": {
|
288
|
+
"id": "1",
|
289
|
+
"type": "posts",
|
290
|
+
"attributes": {
|
291
|
+
"title": "Hello World",
|
292
|
+
"content": "Your first post"
|
293
|
+
},
|
294
|
+
"links": {
|
295
|
+
"self": "http://example.com/posts/1"
|
296
|
+
},
|
297
|
+
"relationships": {
|
298
|
+
"author": {
|
299
|
+
"links": {
|
300
|
+
"self": "http://example.com/posts/1/relationships/author",
|
301
|
+
"related": "http://example.com/posts/1/author"
|
302
|
+
}
|
303
|
+
},
|
304
|
+
"comments": {
|
305
|
+
"links": {
|
306
|
+
"self": "http://example.com/posts/1/relationships/comments",
|
307
|
+
"related": "http://example.com/posts/1/comments"
|
308
|
+
},
|
309
|
+
}
|
310
|
+
}
|
311
|
+
}
|
312
|
+
}
|
313
|
+
```
|
314
|
+
|
315
|
+
Alternatively, you can specify `base_url` as an argument to `serialize` which allows you to build the URL with different subdomains or other logic from the request:
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
JSONAPI::Serializer.serialize(post, base_url: 'http://example.com')
|
319
|
+
```
|
320
|
+
|
321
|
+
Note: if you override `self_link` in your serializer and leave out `base_url`, it will not be included.
|
322
|
+
|
323
|
+
### Root metadata
|
324
|
+
|
325
|
+
You can pass a `meta` argument to specify top-level metadata:
|
326
|
+
|
327
|
+
```ruby
|
328
|
+
JSONAPI::Serializer.serialize(post, meta: {copyright: 'Copyright 2015 Example Corp.'})
|
329
|
+
```
|
330
|
+
|
331
|
+
### Root links
|
332
|
+
|
333
|
+
You can pass a `links` argument to specify top-level links:
|
334
|
+
|
335
|
+
```ruby
|
336
|
+
JSONAPI::Serializer.serialize(post, links: {self: 'https://example.com/posts'})
|
337
|
+
```
|
338
|
+
|
339
|
+
### Root errors
|
340
|
+
|
341
|
+
You can use `serialize_errors` method in order to specify top-level errors:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
errors = [{ "title": "Invalid Attribute", "detail": "First name must contain at least three characters." }]
|
345
|
+
JSONAPI::Serializer.serialize_errors(errors)
|
346
|
+
```
|
347
|
+
|
348
|
+
If you are using Rails models (ActiveModel by default), you can pass in an object's errors:
|
349
|
+
|
350
|
+
```ruby
|
351
|
+
JSONAPI::Serializer.serialize_errors(user.errors)
|
352
|
+
```
|
353
|
+
|
354
|
+
A more complete usage example (assumes ActiveModel):
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
class Api::V1::ReposController < Api::V1::BaseController
|
358
|
+
def create
|
359
|
+
post = Post.create(post_params)
|
360
|
+
if post.errors
|
361
|
+
render json: JSONAPI::Serializer.serialize_errors(post.errors)
|
362
|
+
else
|
363
|
+
render json: JSONAPI::Serializer.serialize(post)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
```
|
368
|
+
|
369
|
+
### Root 'jsonapi' object
|
370
|
+
|
371
|
+
You can pass a `jsonapi` argument to specify a [top-level "jsonapi" key](http://jsonapi.org/format/#document-jsonapi-object) containing the version of JSON:API in use:
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
JSONAPI::Serializer.serialize(post, jsonapi: {version: '1.0'})
|
375
|
+
```
|
376
|
+
|
377
|
+
### Explicit serializer discovery
|
378
|
+
|
379
|
+
By default, jsonapi-serializers assumes that the serializer class for `Namespace::User` is `Namespace::UserSerializer`. You can override this behavior on a per-object basis by implementing the `jsonapi_serializer_class_name` method.
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
class User
|
383
|
+
def jsonapi_serializer_class_name
|
384
|
+
'SomeOtherNamespace::CustomUserSerializer'
|
385
|
+
end
|
386
|
+
end
|
387
|
+
```
|
388
|
+
|
389
|
+
Now, when a `User` object is serialized, it will use the `SomeOtherNamespace::CustomUserSerializer`.
|
390
|
+
|
391
|
+
### Namespace serializers
|
392
|
+
|
393
|
+
Assume you have an API with multiple versions:
|
394
|
+
|
395
|
+
```ruby
|
396
|
+
module Api
|
397
|
+
module V1
|
398
|
+
class PostSerializer
|
399
|
+
include JSONAPI::Serializer
|
400
|
+
attribute :title
|
401
|
+
end
|
402
|
+
end
|
403
|
+
module V2
|
404
|
+
class PostSerializer
|
405
|
+
include JSONAPI::Serializer
|
406
|
+
attribute :name
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
```
|
411
|
+
|
412
|
+
With the namespace option you can choose which serializer is used.
|
413
|
+
|
414
|
+
```ruby
|
415
|
+
JSONAPI::Serializer.serialize(post, namespace: Api::V1)
|
416
|
+
JSONAPI::Serializer.serialize(post, namespace: Api::V2)
|
417
|
+
```
|
418
|
+
|
419
|
+
This option overrides the `jsonapi_serializer_class_name` method.
|
420
|
+
|
421
|
+
### Sparse fieldsets
|
422
|
+
|
423
|
+
The JSON:API spec allows to return only [specific fields](http://jsonapi.org/format/#fetching-sparse-fieldsets) from attributes and relationships.
|
424
|
+
|
425
|
+
For example, if you wanted to return only the `title` field and `author` relationship link for `posts`:
|
426
|
+
|
427
|
+
```ruby
|
428
|
+
fields =
|
429
|
+
JSONAPI::Serializer.serialize(post, fields: {posts: [:title]})
|
430
|
+
```
|
431
|
+
|
432
|
+
Sparse fieldsets also affect relationship links. In this case, only the `author` relationship link would be included:
|
433
|
+
|
434
|
+
``` ruby
|
435
|
+
JSONAPI::Serializer.serialize(post, fields: {posts: [:title, :author]})
|
436
|
+
```
|
437
|
+
|
438
|
+
Sparse fieldsets operate on a per-type basis, so they affect all resources in the response including in compound documents. For example, this will affect both the `posts` type in the primary data and the `users` type in the compound data:
|
439
|
+
|
440
|
+
``` ruby
|
441
|
+
JSONAPI::Serializer.serialize(
|
442
|
+
post,
|
443
|
+
fields: {posts: ['title', 'author'], users: ['name']},
|
444
|
+
include: 'author',
|
445
|
+
)
|
446
|
+
```
|
447
|
+
|
448
|
+
Sparse fieldsets support comma-separated strings (`fields: {posts: 'title,author'}`, arrays of strings (`fields: {posts: ['title', 'author']}`), single symbols (`fields: {posts: :title}`), and arrays of symbols (`fields: {posts: [:title, :author]}`).
|
449
|
+
|
450
|
+
## Relationships
|
451
|
+
|
452
|
+
You can easily specify relationships with the `has_one` and `has_many` directives.
|
453
|
+
|
454
|
+
```ruby
|
455
|
+
class BaseSerializer
|
456
|
+
include JSONAPI::Serializer
|
457
|
+
end
|
458
|
+
|
459
|
+
class PostSerializer < BaseSerializer
|
460
|
+
attribute :title
|
461
|
+
attribute :content
|
462
|
+
|
463
|
+
has_one :author
|
464
|
+
has_many :comments
|
465
|
+
end
|
466
|
+
|
467
|
+
class UserSerializer < BaseSerializer
|
468
|
+
attribute :name
|
469
|
+
end
|
470
|
+
|
471
|
+
class CommentSerializer < BaseSerializer
|
472
|
+
attribute :content
|
473
|
+
|
474
|
+
has_one :user
|
475
|
+
end
|
476
|
+
```
|
477
|
+
|
478
|
+
Note that when serializing a post, the `author` association will come from the `author` attribute on the `Post` instance, no matter what type it is (in this case it is a `User`). This will work just fine, because JSONAPI::Serializers automatically finds serializer classes by appending `Serializer` to the object's class name. This behavior can be customized.
|
479
|
+
|
480
|
+
Because the full class name is used when discovering serializers, JSONAPI::Serializers works with any custom namespaces you might have, like a Rails Engine or standard Ruby module namespace.
|
481
|
+
|
482
|
+
### Compound documents and includes
|
483
|
+
|
484
|
+
> To reduce the number of HTTP requests, servers MAY allow responses that include related resources along with the requested primary resources. Such responses are called "compound documents".
|
485
|
+
> [JSON:API Compound Documents](http://jsonapi.org/format/#document-structure-compound-documents)
|
486
|
+
|
487
|
+
JSONAPI::Serializers supports compound documents with a simple `include` parameter.
|
488
|
+
|
489
|
+
For example:
|
490
|
+
|
491
|
+
```ruby
|
492
|
+
JSONAPI::Serializer.serialize(post, include: ['author', 'comments', 'comments.user'])
|
493
|
+
```
|
494
|
+
|
495
|
+
Returns:
|
496
|
+
|
497
|
+
```json
|
498
|
+
{
|
499
|
+
"data": {
|
500
|
+
"id": "1",
|
501
|
+
"type": "posts",
|
502
|
+
"attributes": {
|
503
|
+
"title": "Hello World",
|
504
|
+
"content": "Your first post"
|
505
|
+
},
|
506
|
+
"links": {
|
507
|
+
"self": "/posts/1"
|
508
|
+
},
|
509
|
+
"relationships": {
|
510
|
+
"author": {
|
511
|
+
"links": {
|
512
|
+
"self": "/posts/1/relationships/author",
|
513
|
+
"related": "/posts/1/author"
|
514
|
+
},
|
515
|
+
"data": {
|
516
|
+
"type": "users",
|
517
|
+
"id": "1"
|
518
|
+
}
|
519
|
+
},
|
520
|
+
"comments": {
|
521
|
+
"links": {
|
522
|
+
"self": "/posts/1/relationships/comments",
|
523
|
+
"related": "/posts/1/comments"
|
524
|
+
},
|
525
|
+
"data": [
|
526
|
+
{
|
527
|
+
"type": "comments",
|
528
|
+
"id": "1"
|
529
|
+
}
|
530
|
+
]
|
531
|
+
}
|
532
|
+
}
|
533
|
+
},
|
534
|
+
"included": [
|
535
|
+
{
|
536
|
+
"id": "1",
|
537
|
+
"type": "users",
|
538
|
+
"attributes": {
|
539
|
+
"name": "Post Author"
|
540
|
+
},
|
541
|
+
"links": {
|
542
|
+
"self": "/users/1"
|
543
|
+
}
|
544
|
+
},
|
545
|
+
{
|
546
|
+
"id": "1",
|
547
|
+
"type": "comments",
|
548
|
+
"attributes": {
|
549
|
+
"content": "Have no fear, sers, your king is safe."
|
550
|
+
},
|
551
|
+
"links": {
|
552
|
+
"self": "/comments/1"
|
553
|
+
},
|
554
|
+
"relationships": {
|
555
|
+
"user": {
|
556
|
+
"links": {
|
557
|
+
"self": "/comments/1/relationships/user",
|
558
|
+
"related": "/comments/1/user"
|
559
|
+
},
|
560
|
+
"data": {
|
561
|
+
"type": "users",
|
562
|
+
"id": "2"
|
563
|
+
}
|
564
|
+
},
|
565
|
+
"post": {
|
566
|
+
"links": {
|
567
|
+
"self": "/comments/1/relationships/post",
|
568
|
+
"related": "/comments/1/post"
|
569
|
+
}
|
570
|
+
}
|
571
|
+
}
|
572
|
+
},
|
573
|
+
{
|
574
|
+
"id": "2",
|
575
|
+
"type": "users",
|
576
|
+
"attributes": {
|
577
|
+
"name": "Barristan Selmy"
|
578
|
+
},
|
579
|
+
"links": {
|
580
|
+
"self": "/users/2"
|
581
|
+
}
|
582
|
+
}
|
583
|
+
]
|
584
|
+
}
|
585
|
+
```
|
586
|
+
|
587
|
+
Notice a few things:
|
588
|
+
* The [primary data](http://jsonapi.org/format/#document-structure-top-level) relationships now include "linkage" information for each relationship that was included.
|
589
|
+
* The related objects themselves are loaded in the top-level `included` member.
|
590
|
+
* The related objects _also_ include "linkage" data when a deeper relationship is also present in the compound document. This is a very powerful feature of the JSON:API spec, and allows you to deeply link complicated relationships all in the same document and in a single HTTP response. JSONAPI::Serializers automatically includes the correct linkage data for whatever `include` paths you specify. This conforms to this part of the spec:
|
591
|
+
|
592
|
+
> Note: Full linkage ensures that included resources are related to either the primary data (which could be resource objects or resource identifier objects) or to each other.
|
593
|
+
> [JSON:API Compound Documents](http://jsonapi.org/format/#document-compound-documents)
|
594
|
+
|
595
|
+
#### Relationship path handling
|
596
|
+
|
597
|
+
The `include` param also accepts a string of [relationship paths](http://jsonapi.org/format/#fetching-includes), ie. `include: 'author,comments,comments.user'` so you can pass an `?include` query param directly through to the serialize method. Be aware that letting users pass arbitrary relationship paths might introduce security issues depending on your authorization setup, where a user could `include` a relationship they might not be authorized to see directly. Be aware of what you allow API users to include.
|
598
|
+
|
599
|
+
### Control `links` and `data` in relationships
|
600
|
+
|
601
|
+
The JSON API spec allows relationships objects to contain `links`, `data`, or both.
|
602
|
+
|
603
|
+
By default, `links` are included in each relationship. You can remove links for a specific relationship by passing `include_links: false` to `has_one` or `has_many`. For example:
|
604
|
+
|
605
|
+
```ruby
|
606
|
+
has_many :comments, include_links: false # Default is include_links: true.
|
607
|
+
```
|
608
|
+
|
609
|
+
Notice that `links` are now excluded for the `comments` relationship:
|
610
|
+
|
611
|
+
```json
|
612
|
+
"relationships": {
|
613
|
+
"author": {
|
614
|
+
"links": {
|
615
|
+
"self": "/posts/1/relationships/author",
|
616
|
+
"related": "/posts/1/author"
|
617
|
+
}
|
618
|
+
},
|
619
|
+
"comments": {}
|
620
|
+
}
|
621
|
+
```
|
622
|
+
|
623
|
+
By default, `data` is excluded in each relationship. You can enable data for a specific relationship by passing `include_data: true` to `has_one` or `has_many`. For example:
|
624
|
+
|
625
|
+
```ruby
|
626
|
+
has_one :author, include_data: true # Default is include_data: false.
|
627
|
+
```
|
628
|
+
|
629
|
+
Notice that linkage data is now included for the `author` relationship:
|
630
|
+
|
631
|
+
```json
|
632
|
+
"relationships": {
|
633
|
+
"author": {
|
634
|
+
"links": {
|
635
|
+
"self": "/posts/1/relationships/author",
|
636
|
+
"related": "/posts/1/author"
|
637
|
+
},
|
638
|
+
"data": {
|
639
|
+
"type": "users",
|
640
|
+
"id": "1"
|
641
|
+
}
|
642
|
+
}
|
643
|
+
```
|
644
|
+
|
645
|
+
## Instrumentation
|
646
|
+
|
647
|
+
Using [ActiveSupport::Notifications](https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) you can subscribe to key notifications to better understand the performance of your serialization.
|
648
|
+
|
649
|
+
The following notifications can be subscribed to:
|
650
|
+
|
651
|
+
* `render.jsonapi_serializers.serialize_primary` - time spent serializing a single object
|
652
|
+
* `render.jsonapi_serializers.find_recursive_relationships` - time spent finding objects to serialize through relationships
|
653
|
+
|
654
|
+
This is an example of how you might subscribe to all events that come from `jsonapi-serializers`.
|
655
|
+
|
656
|
+
```ruby
|
657
|
+
require 'active_support/notifications'
|
658
|
+
|
659
|
+
ActiveSupport::Notifications.subscribe(/^render\.jsonapi_serializers\..*/) do |*args|
|
660
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
661
|
+
|
662
|
+
puts event.name
|
663
|
+
puts event.time
|
664
|
+
puts event.end
|
665
|
+
puts event.payload
|
666
|
+
end
|
667
|
+
```
|
668
|
+
|
669
|
+
## Rails example
|
670
|
+
|
671
|
+
```ruby
|
672
|
+
# app/serializers/base_serializer.rb
|
673
|
+
class BaseSerializer
|
674
|
+
include JSONAPI::Serializer
|
675
|
+
|
676
|
+
def self_link
|
677
|
+
"/api/v1#{super}"
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
# app/serializers/post_serializer.rb
|
682
|
+
class PostSerializer < BaseSerializer
|
683
|
+
attribute :title
|
684
|
+
attribute :content
|
685
|
+
end
|
686
|
+
|
687
|
+
# app/controllers/api/v1/base_controller.rb
|
688
|
+
class Api::V1::BaseController < ActionController::Base
|
689
|
+
# Convenience methods for serializing models:
|
690
|
+
def serialize_model(model, options = {})
|
691
|
+
options[:is_collection] = false
|
692
|
+
JSONAPI::Serializer.serialize(model, options)
|
693
|
+
end
|
694
|
+
|
695
|
+
def serialize_models(models, options = {})
|
696
|
+
options[:is_collection] = true
|
697
|
+
JSONAPI::Serializer.serialize(models, options)
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
# app/controllers/api/v1/posts_controller.rb
|
702
|
+
class Api::V1::ReposController < Api::V1::BaseController
|
703
|
+
def index
|
704
|
+
posts = Post.all
|
705
|
+
render json: serialize_models(posts)
|
706
|
+
end
|
707
|
+
|
708
|
+
def show
|
709
|
+
post = Post.find(params[:id])
|
710
|
+
render json: serialize_model(post)
|
711
|
+
end
|
712
|
+
end
|
713
|
+
|
714
|
+
# config/initializers/jsonapi_mimetypes.rb
|
715
|
+
# Without this mimetype registration, controllers will not automatically parse JSON API params.
|
716
|
+
|
717
|
+
# Rails 4
|
718
|
+
module JSONAPI
|
719
|
+
MIMETYPE = "application/vnd.api+json"
|
720
|
+
end
|
721
|
+
Mime::Type.register(JSONAPI::MIMETYPE, :api_json)
|
722
|
+
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime::Type.lookup(JSONAPI::MIMETYPE)] = lambda do |body|
|
723
|
+
JSON.parse(body)
|
724
|
+
end
|
725
|
+
|
726
|
+
# Rails 5 Option 1: Add another synonym to the json mime type
|
727
|
+
json_mime_type_synonyms = %w[
|
728
|
+
text/x-json
|
729
|
+
application/jsonrequest
|
730
|
+
application/vnd.api+json
|
731
|
+
]
|
732
|
+
Mime::Type.register('application/json', :json, json_mime_type_synonyms)
|
733
|
+
|
734
|
+
# Rails 5 Option 2: Add a separate mime type
|
735
|
+
Mime::Type.register('application/vnd.api+json', :api_json)
|
736
|
+
ActionDispatch::Request.parameter_parsers[:api_json] = -> (body) {
|
737
|
+
JSON.parse(body)
|
738
|
+
}
|
739
|
+
```
|
740
|
+
|
741
|
+
## Sinatra example
|
742
|
+
|
743
|
+
Here's an example using [Sinatra](http://www.sinatrarb.com) and
|
744
|
+
[Sequel ORM](http://sequel.jeremyevans.net) instead of Rails and ActiveRecord.
|
745
|
+
The important takeaways here are that:
|
746
|
+
|
747
|
+
1. The `:tactical_eager_loading` plugin will greatly reduce the number of
|
748
|
+
queries performed when sideloading associated records. You can add this
|
749
|
+
plugin to a single model (as demonstrated here), or globally to all models.
|
750
|
+
For more information, please see the Sequel
|
751
|
+
[documentation](http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/TacticalEagerLoading.html).
|
752
|
+
1. The `:skip_collection_check` option must be set to true in order for
|
753
|
+
JSONAPI::Serializer to be able to serialize a single Sequel::Model instance.
|
754
|
+
1. You should call `#all` on your Sequel::Dataset instances before passing them
|
755
|
+
to JSONAPI::Serializer to greatly reduce the number of queries performed.
|
756
|
+
|
757
|
+
```ruby
|
758
|
+
require 'sequel'
|
759
|
+
require 'sinatra/base'
|
760
|
+
require 'json'
|
761
|
+
require 'jsonapi-serializers'
|
762
|
+
|
763
|
+
class Post < Sequel::Model
|
764
|
+
plugin :tactical_eager_loading
|
765
|
+
|
766
|
+
one_to_many :comments
|
767
|
+
end
|
768
|
+
|
769
|
+
class Comment < Sequel::Model
|
770
|
+
many_to_one :post
|
771
|
+
end
|
772
|
+
|
773
|
+
class BaseSerializer
|
774
|
+
include JSONAPI::Serializer
|
775
|
+
|
776
|
+
def self_link
|
777
|
+
"/api/v1#{super}"
|
778
|
+
end
|
779
|
+
end
|
780
|
+
|
781
|
+
class PostSerializer < BaseSerializer
|
782
|
+
attributes :title, :content
|
783
|
+
|
784
|
+
has_many :comments
|
785
|
+
end
|
786
|
+
|
787
|
+
class CommentSerializer < BaseSerializer
|
788
|
+
attributes :username, :content
|
789
|
+
|
790
|
+
has_one :post
|
791
|
+
end
|
792
|
+
|
793
|
+
module Api
|
794
|
+
class V1 < Sinatra::Base
|
795
|
+
configure do
|
796
|
+
mime_type :api_json, 'application/vnd.api+json'
|
797
|
+
|
798
|
+
set :database, Sequel.connect
|
799
|
+
end
|
800
|
+
|
801
|
+
helpers do
|
802
|
+
def parse_request_body
|
803
|
+
return unless request.body.respond_to?(:size) &&
|
804
|
+
request.body.size > 0
|
805
|
+
|
806
|
+
halt 415 unless request.content_type &&
|
807
|
+
request.content_type[/^[^;]+/] == mime_type(:api_json)
|
808
|
+
|
809
|
+
request.body.rewind
|
810
|
+
JSON.parse(request.body.read, symbolize_names: true)
|
811
|
+
end
|
812
|
+
|
813
|
+
# Convenience methods for serializing models:
|
814
|
+
def serialize_model(model, options = {})
|
815
|
+
options[:is_collection] = false
|
816
|
+
options[:skip_collection_check] = true
|
817
|
+
JSONAPI::Serializer.serialize(model, options)
|
818
|
+
end
|
819
|
+
|
820
|
+
def serialize_models(models, options = {})
|
821
|
+
options[:is_collection] = true
|
822
|
+
JSONAPI::Serializer.serialize(models, options)
|
823
|
+
end
|
824
|
+
end
|
825
|
+
|
826
|
+
before do
|
827
|
+
halt 406 unless request.preferred_type.entry == mime_type(:api_json)
|
828
|
+
@data = parse_request_body
|
829
|
+
content_type :api_json
|
830
|
+
end
|
831
|
+
|
832
|
+
get '/posts' do
|
833
|
+
posts = Post.all
|
834
|
+
serialize_models(posts).to_json
|
835
|
+
end
|
836
|
+
|
837
|
+
get '/posts/:id' do
|
838
|
+
post = Post[params[:id].to_i]
|
839
|
+
not_found if post.nil?
|
840
|
+
serialize_model(post, include: 'comments').to_json
|
841
|
+
end
|
842
|
+
end
|
843
|
+
end
|
844
|
+
```
|
845
|
+
|
846
|
+
See also: [Sinja](https://github.com/mwpastore/sinja), which extends Sinatra
|
847
|
+
and leverages jsonapi-serializers to provide a JSON:API framework.
|
848
|
+
|
849
|
+
## Changelog
|
850
|
+
|
851
|
+
See [Releases](https://github.com/fotinakis/jsonapi-serializers/releases).
|
852
|
+
|
853
|
+
## Unfinished business
|
854
|
+
|
855
|
+
* Support for pagination/sorting is unlikely to be supported because it would likely involve coupling to ActiveRecord, but please open an issue if you have ideas of how to support this generically.
|
856
|
+
|
857
|
+
## Contributing
|
858
|
+
|
859
|
+
1. Fork it ( https://github.com/fotinakis/jsonapi-serializers/fork )
|
860
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
861
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
862
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
863
|
+
5. Create a new Pull Request
|
864
|
+
|
865
|
+
Throw a ★ on it! :)
|
866
|
+
|
867
|
+
# Forked from https://github.com/fotinakis/jsonapi-serializers
|
868
|
+
Add a new namespace to avoid conflict with the `jsonapi-serializer` gem.
|