json_serializers 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +61 -0
- data/README.md +699 -0
- data/lib/json_serializers/compat.rb +66 -0
- data/lib/json_serializers/controller_serialization.rb +32 -0
- data/lib/json_serializers/json_string_encoder.rb +41 -0
- data/lib/json_serializers/json_value.rb +34 -0
- data/lib/json_serializers/memo.rb +20 -0
- data/lib/json_serializers/serializer.rb +726 -0
- data/lib/json_serializers/setup.rb +31 -0
- data/lib/json_serializers/sugar.rb +17 -0
- data/lib/json_serializers/version.rb +5 -0
- data/lib/json_serializers.rb +6 -0
- metadata +61 -0
data/README.md
ADDED
@@ -0,0 +1,699 @@
|
|
1
|
+
<h1 align="center">
|
2
|
+
Oj Serializers
|
3
|
+
<p align="center">
|
4
|
+
<a href="https://github.com/ElMassimo/oj_serializers/actions"><img alt="Build Status" src="https://github.com/ElMassimo/oj_serializers/workflows/build/badge.svg"/></a>
|
5
|
+
<a href="https://codeclimate.com/github/ElMassimo/oj_serializers"><img alt="Maintainability" src="https://codeclimate.com/github/ElMassimo/oj_serializers/badges/gpa.svg"/></a>
|
6
|
+
<a href="https://codeclimate.com/github/ElMassimo/oj_serializers"><img alt="Test Coverage" src="https://codeclimate.com/github/ElMassimo/oj_serializers/badges/coverage.svg"/></a>
|
7
|
+
<a href="https://rubygems.org/gems/oj_serializers"><img alt="Gem Version" src="https://img.shields.io/gem/v/oj_serializers.svg?colorB=e9573f"/></a>
|
8
|
+
<a href="https://github.com/ElMassimo/oj_serializers/blob/main/LICENSE.txt"><img alt="License" src="https://img.shields.io/badge/license-MIT-428F7E.svg"/></a>
|
9
|
+
</p>
|
10
|
+
</h1>
|
11
|
+
|
12
|
+
Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] library.
|
13
|
+
|
14
|
+
[oj]: https://github.com/ohler55/oj
|
15
|
+
[mongoid]: https://github.com/mongodb/mongoid
|
16
|
+
[ams]: https://github.com/rails-api/active_model_serializers
|
17
|
+
[jsonapi]: https://github.com/jsonapi-serializer/jsonapi-serializer
|
18
|
+
[panko]: https://github.com/panko-serializer/panko_serializer
|
19
|
+
[blueprinter]: https://github.com/procore/blueprinter
|
20
|
+
[benchmarks]: https://github.com/ElMassimo/oj_serializers/tree/master/benchmarks
|
21
|
+
[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/main/benchmarks/document_benchmark.rb
|
22
|
+
[sugar]: https://github.com/ElMassimo/oj_serializers/blob/main/lib/oj_serializers/sugar.rb#L14
|
23
|
+
[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/main/MIGRATION_GUIDE.md
|
24
|
+
[design]: https://github.com/ElMassimo/oj_serializers#design-
|
25
|
+
[associations]: https://github.com/ElMassimo/oj_serializers#associations-
|
26
|
+
[compose]: https://github.com/ElMassimo/oj_serializers#composing-serializers-
|
27
|
+
[raw_json]: https://github.com/ohler55/oj/issues/542
|
28
|
+
[trailing_commas]: https://maximomussini.com/posts/trailing-commas/
|
29
|
+
[render dsl]: https://github.com/ElMassimo/oj_serializers#render-dsl-
|
30
|
+
[sorbet]: https://sorbet.org/
|
31
|
+
[Discussion]: https://github.com/ElMassimo/oj_serializers/discussions
|
32
|
+
[TypeScript]: https://www.typescriptlang.org/
|
33
|
+
[types_from_serializers]: https://github.com/ElMassimo/types_from_serializers
|
34
|
+
[inheritance]: https://github.com/ElMassimo/types_from_serializers/blob/main/playground/vanilla/app/serializers/song_with_videos_serializer.rb#L1
|
35
|
+
|
36
|
+
## Why? 🤔
|
37
|
+
|
38
|
+
[`ActiveModel::Serializer`][ams] has a nice DSL, but it allocates many objects
|
39
|
+
leading to memory bloat, time spent on GC, and lower performance.
|
40
|
+
|
41
|
+
`JsonSerializer` provides a similar API, with [better performance][benchmarks].
|
42
|
+
|
43
|
+
Learn more about [how this library achieves its performance][design].
|
44
|
+
|
45
|
+
## Features ⚡️
|
46
|
+
|
47
|
+
- Intuitive declaration syntax, supporting mixins and inheritance
|
48
|
+
- Reduced [memory allocation][benchmarks] and [improved performance][benchmarks]
|
49
|
+
- Generate [TypeScript interfaces automatically][types_from_serializers]
|
50
|
+
- Support for [`has_one`][associations] and [`has_many`][associations], compose with [`flat_one`][compose]
|
51
|
+
- Useful development checks to avoid typos and mistakes
|
52
|
+
- [Migrate easily from Active Model Serializers][migration guide]
|
53
|
+
|
54
|
+
## Installation 💿
|
55
|
+
|
56
|
+
Add this line to your application's Gemfile:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
gem 'json_serializers'
|
60
|
+
```
|
61
|
+
|
62
|
+
And then run:
|
63
|
+
|
64
|
+
$ bundle install
|
65
|
+
|
66
|
+
## Usage 🚀
|
67
|
+
|
68
|
+
You can define a serializer by subclassing `JsonSerializer`, and specify which
|
69
|
+
attributes should be serialized.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class AlbumSerializer < JsonSerializer
|
73
|
+
attributes :name, :genres
|
74
|
+
|
75
|
+
attribute :release do
|
76
|
+
album.release_date.strftime('%B %d, %Y')
|
77
|
+
end
|
78
|
+
|
79
|
+
has_many :songs, serializer: SongSerializer
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
<details>
|
84
|
+
<summary>Example Output</summary>
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
{
|
88
|
+
name: "Abraxas",
|
89
|
+
genres: [
|
90
|
+
"Pyschodelic Rock",
|
91
|
+
"Blues Rock",
|
92
|
+
"Jazz Fusion",
|
93
|
+
"Latin Rock",
|
94
|
+
],
|
95
|
+
release: "September 23, 1970",
|
96
|
+
songs: [
|
97
|
+
{
|
98
|
+
track: 1,
|
99
|
+
name: "Sing Winds, Crying Beasts",
|
100
|
+
composers: ["Michael Carabello"],
|
101
|
+
},
|
102
|
+
{
|
103
|
+
track: 2,
|
104
|
+
name: "Black Magic Woman / Gypsy Queen",
|
105
|
+
composers: ["Peter Green", "Gábor Szabó"],
|
106
|
+
},
|
107
|
+
{
|
108
|
+
track: 3,
|
109
|
+
name: "Oye como va",
|
110
|
+
composers: ["Tito Puente"],
|
111
|
+
},
|
112
|
+
{
|
113
|
+
track: 4,
|
114
|
+
name: "Incident at Neshabur",
|
115
|
+
composers: ["Alberto Gianquinto", "Carlos Santana"],
|
116
|
+
},
|
117
|
+
{
|
118
|
+
track: 5,
|
119
|
+
name: "Se acabó",
|
120
|
+
composers: ["José Areas"],
|
121
|
+
},
|
122
|
+
{
|
123
|
+
track: 6,
|
124
|
+
name: "Mother's Daughter",
|
125
|
+
composers: ["Gregg Rolie"],
|
126
|
+
},
|
127
|
+
{
|
128
|
+
track: 7,
|
129
|
+
name: "Samba pa ti",
|
130
|
+
composers: ["Santana"],
|
131
|
+
},
|
132
|
+
{
|
133
|
+
track: 8,
|
134
|
+
name: "Hope You're Feeling Better",
|
135
|
+
composers: ["Rolie"],
|
136
|
+
},
|
137
|
+
{
|
138
|
+
track: 9,
|
139
|
+
name: "El Nicoya",
|
140
|
+
composers: ["Areas"],
|
141
|
+
},
|
142
|
+
],
|
143
|
+
}
|
144
|
+
```
|
145
|
+
</details>
|
146
|
+
|
147
|
+
You can then use your new serializer to render an object or collection:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class AlbumsController < ApplicationController
|
151
|
+
def show
|
152
|
+
render json: AlbumSerializer.one(album)
|
153
|
+
end
|
154
|
+
|
155
|
+
def index
|
156
|
+
render json: { albums: AlbumSerializer.many(albums) }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
<details>
|
162
|
+
<summary>Active Model Serializers style</summary>
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
require "json_serializers/sugar" # In an initializer
|
166
|
+
|
167
|
+
class AlbumsController < ApplicationController
|
168
|
+
def show
|
169
|
+
render json: album, serializer: AlbumSerializer
|
170
|
+
end
|
171
|
+
|
172
|
+
def index
|
173
|
+
render json: albums, root: :albums, each_serializer: AlbumSerializer
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
</details>
|
178
|
+
|
179
|
+
## Rendering 🖨
|
180
|
+
|
181
|
+
Use `one` to serialize objects, and `many` to serialize enumerables:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
render json: {
|
185
|
+
favorite_album: AlbumSerializer.one(album),
|
186
|
+
purchased_albums: AlbumSerializer.many(albums),
|
187
|
+
}
|
188
|
+
```
|
189
|
+
|
190
|
+
Serializers can be rendered arrays, hashes, or even inside `ActiveModel::Serializer`
|
191
|
+
by using a method in the serializer, making it very easy to combine with other
|
192
|
+
libraries and migrate incrementally.
|
193
|
+
|
194
|
+
`render` is a shortcut for `one` and `many`:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
render json: {
|
198
|
+
favorite_album: AlbumSerializer.render(album),
|
199
|
+
purchased_albums: AlbumSerializer.render(albums),
|
200
|
+
}
|
201
|
+
```
|
202
|
+
|
203
|
+
## Attributes DSL 🪄
|
204
|
+
|
205
|
+
Specify which attributes should be rendered by calling a method in the object to serialize.
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
class PlayerSerializer < JsonSerializer
|
209
|
+
attributes :first_name, :last_name, :full_name
|
210
|
+
end
|
211
|
+
```
|
212
|
+
|
213
|
+
You can serialize custom values by specifying that a method is an `attribute`:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
class PlayerSerializer < JsonSerializer
|
217
|
+
attribute :name do
|
218
|
+
"#{player.first_name} #{player.last_name}"
|
219
|
+
end
|
220
|
+
|
221
|
+
# or
|
222
|
+
|
223
|
+
attribute
|
224
|
+
def name
|
225
|
+
"#{player.first_name} #{player.last_name}"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
> **Note**
|
231
|
+
>
|
232
|
+
> In this example, `player` was inferred from `PlayerSerializer`.
|
233
|
+
>
|
234
|
+
> You can customize this by using [`object_as`](#using-a-different-alias-for-the-internal-object).
|
235
|
+
|
236
|
+
|
237
|
+
### Associations 🔗
|
238
|
+
|
239
|
+
Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
|
240
|
+
|
241
|
+
You must specificy which serializer to use with the `serializer` option.
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
class SongSerializer < JsonSerializer
|
245
|
+
has_one :album, serializer: AlbumSerializer
|
246
|
+
has_many :composers, serializer: ComposerSerializer
|
247
|
+
end
|
248
|
+
```
|
249
|
+
|
250
|
+
Specify a different value for the association by providing a block:
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
class SongSerializer < JsonSerializer
|
254
|
+
has_one :album, serializer: AlbumSerializer do
|
255
|
+
Album.find_by(song_ids: song.id)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
```
|
259
|
+
|
260
|
+
In case you need to pass options, you can call the serializer manually:
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
class SongSerializer < JsonSerializer
|
264
|
+
attribute :album do
|
265
|
+
AlbumSerializer.one(song.album, for_song: song)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
### Aliasing or renaming attributes ↔️
|
271
|
+
|
272
|
+
You can pass `as` when defining an attribute or association to serialize it
|
273
|
+
using a different key:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
class SongSerializer < JsonSerializer
|
277
|
+
has_one :album, as: :first_release, serializer: AlbumSerializer
|
278
|
+
|
279
|
+
attributes title: {as: :name}
|
280
|
+
|
281
|
+
# or as a shortcut
|
282
|
+
attributes title: :name
|
283
|
+
end
|
284
|
+
```
|
285
|
+
|
286
|
+
### Conditional attributes ❔
|
287
|
+
|
288
|
+
You can render attributes and associations conditionally by using `:if`.
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
class PlayerSerializer < JsonSerializer
|
292
|
+
attributes :first_name, :last_name, if: -> { player.display_name? }
|
293
|
+
|
294
|
+
has_one :album, serializer: AlbumSerializer, if: -> { player.album }
|
295
|
+
end
|
296
|
+
```
|
297
|
+
|
298
|
+
This is useful in cases where you don't want to `null` values to be in the response.
|
299
|
+
|
300
|
+
## Advanced Usage 🧙♂️
|
301
|
+
|
302
|
+
### Using a different alias for the internal object
|
303
|
+
|
304
|
+
In most cases, the default alias for the `object` will be convenient enough.
|
305
|
+
|
306
|
+
However, if you would like to specify it manually, use `object_as`:
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
class DiscographySerializer < JsonSerializer
|
310
|
+
object_as :artist
|
311
|
+
|
312
|
+
# Now we can use `artist` instead of `object` or `discography`.
|
313
|
+
attribute
|
314
|
+
def latest_albums
|
315
|
+
artist.albums.desc(:year)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
### Identifier attributes
|
321
|
+
|
322
|
+
The `identifier` method allows you to only include an identifier if the record
|
323
|
+
or document has been persisted.
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
class AlbumSerializer < JsonSerializer
|
327
|
+
identifier
|
328
|
+
|
329
|
+
# or if it's a different field
|
330
|
+
identifier :uuid
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
Additionally, identifier fields are always rendered first, even when sorting
|
335
|
+
fields alphabetically.
|
336
|
+
|
337
|
+
### Transforming attribute keys 🗝
|
338
|
+
|
339
|
+
When serialized data will be consumed from a client language that has different
|
340
|
+
naming conventions, it can be convenient to transform keys accordingly.
|
341
|
+
|
342
|
+
For example, when rendering an API to be consumed from the browser via JavaScript,
|
343
|
+
where properties are traditionally named using camel case.
|
344
|
+
|
345
|
+
Use `transform_keys` to handle that conversion.
|
346
|
+
|
347
|
+
```ruby
|
348
|
+
class BaseSerializer < JsonSerializer
|
349
|
+
transform_keys :camelize
|
350
|
+
|
351
|
+
# shortcut for
|
352
|
+
transform_keys -> (key) { key.to_s.camelize(:lower) }
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
This has no performance impact, as keys will be transformed at load time.
|
357
|
+
|
358
|
+
### Sorting attributes 📶
|
359
|
+
|
360
|
+
By default attributes are rendered in the order they are defined.
|
361
|
+
|
362
|
+
If you would like to sort attributes alphabetically, you can specify it at a
|
363
|
+
serializer level:
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
class BaseSerializer < JsonSerializer
|
367
|
+
sort_attributes_by :name # or a Proc
|
368
|
+
end
|
369
|
+
```
|
370
|
+
|
371
|
+
This has no performance impact, as attributes will be sorted at load time.
|
372
|
+
|
373
|
+
### Path helpers 🛣
|
374
|
+
|
375
|
+
In case you need to access path helpers in your serializers, you can use the
|
376
|
+
following:
|
377
|
+
|
378
|
+
```ruby
|
379
|
+
class BaseSerializer < JsonSerializer
|
380
|
+
include Rails.application.routes.url_helpers
|
381
|
+
|
382
|
+
def default_url_options
|
383
|
+
Rails.application.routes.default_url_options
|
384
|
+
end
|
385
|
+
end
|
386
|
+
```
|
387
|
+
|
388
|
+
One slight variation that might make it easier to maintain in the long term is
|
389
|
+
to use a separate singleton service to provide the url helpers and options, and
|
390
|
+
make it available as `urls`.
|
391
|
+
|
392
|
+
### Generating TypeScript automatically 🤖
|
393
|
+
|
394
|
+
It's easy for the backend and the frontend to become out of sync. Traditionally, preventing bugs requires writing extensive integration tests.
|
395
|
+
|
396
|
+
[TypeScript] is a great tool to catch this kind of bugs and mistakes, as it can detect incorrect usages and missing fields, but writing types manually is cumbersome, and they can become stale over time, giving a false sense of confidence.
|
397
|
+
|
398
|
+
[`types_from_serializers`][types_from_serializers] extends this library to allow embedding type information, as well as inferring types from the SQL schema when available, and uses this information to automatically generate TypeScript interfaces from your serializers.
|
399
|
+
|
400
|
+
As a result, it's posible to easily detect mismatches between the backend and the frontend, as well as make the fields more discoverable and provide great autocompletion in the frontend, without having to manually write the types.
|
401
|
+
|
402
|
+
### Composing serializers 🧱
|
403
|
+
|
404
|
+
There are three options to [compose serializers](https://github.com/ElMassimo/oj_serializers/discussions/10#discussioncomment-5523921): [inheritance], mixins, and `flat_one`.
|
405
|
+
|
406
|
+
Use `flat_one` to include all attributes from a different serializer:
|
407
|
+
|
408
|
+
```ruby
|
409
|
+
class AttachmentSerializer < BaseSerializer
|
410
|
+
identifier
|
411
|
+
|
412
|
+
class BlobSerializer < BaseSerializer
|
413
|
+
attributes :filename, :byte_size, :content_type, :created_at
|
414
|
+
end
|
415
|
+
|
416
|
+
flat_one :blob, serializer: BlobSerializer
|
417
|
+
end
|
418
|
+
```
|
419
|
+
|
420
|
+
Think of it as `has_one` without a "root", all the attributes are added directly.
|
421
|
+
|
422
|
+
<details>
|
423
|
+
<summary>Example Output</summary>
|
424
|
+
|
425
|
+
```ruby
|
426
|
+
{
|
427
|
+
id: 5,
|
428
|
+
filename: "image.jpg,
|
429
|
+
byte_size: 256074,
|
430
|
+
content_type: "image/jpeg",
|
431
|
+
created_at: "2022-08-04T17:25:12.637-07:00",
|
432
|
+
}
|
433
|
+
```
|
434
|
+
</details>
|
435
|
+
|
436
|
+
This is especially convenient when using [`types_from_serializers`][types_from_serializers],
|
437
|
+
as it enables automatic type inference for the included attributes.
|
438
|
+
|
439
|
+
### Memoization & local state
|
440
|
+
|
441
|
+
Serializers are designed to be stateless so that an instanced can be reused, but
|
442
|
+
sometimes it's convenient to store intermediate calculations.
|
443
|
+
|
444
|
+
Use `memo` for memoization and storing temporary information.
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
class DownloadSerializer < JsonSerializer
|
448
|
+
attributes :filename, :size
|
449
|
+
|
450
|
+
attribute
|
451
|
+
def progress
|
452
|
+
"#{ last_event&.progress || 0 }%"
|
453
|
+
end
|
454
|
+
|
455
|
+
private
|
456
|
+
|
457
|
+
def last_event
|
458
|
+
memo.fetch(:last_event) {
|
459
|
+
download.events.desc(:created_at).first
|
460
|
+
}
|
461
|
+
end
|
462
|
+
end
|
463
|
+
```
|
464
|
+
|
465
|
+
### `hash_attributes` 🚀
|
466
|
+
|
467
|
+
Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator.
|
468
|
+
|
469
|
+
```ruby
|
470
|
+
class PersonSerializer < JsonSerializer
|
471
|
+
hash_attributes 'first_name', :last_name
|
472
|
+
end
|
473
|
+
|
474
|
+
PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
|
475
|
+
# {first_name: "Mary", last_name: "Watson"}
|
476
|
+
```
|
477
|
+
|
478
|
+
### `mongo_attributes` 🚀
|
479
|
+
|
480
|
+
Reads data directly from `attributes` in a [Mongoid] document.
|
481
|
+
|
482
|
+
By skipping type casting, coercion, and defaults, it [achieves the best performance][raw_benchmarks].
|
483
|
+
|
484
|
+
Although there are some downsides, depending on how consistent your schema is,
|
485
|
+
and which kind of consumer the API has, it can be really powerful.
|
486
|
+
|
487
|
+
```ruby
|
488
|
+
class AlbumSerializer < JsonSerializer
|
489
|
+
mongo_attributes :id, :name
|
490
|
+
end
|
491
|
+
```
|
492
|
+
|
493
|
+
### Caching 📦
|
494
|
+
|
495
|
+
Usually rendering is so fast that __turning caching on can be slower__.
|
496
|
+
|
497
|
+
However, in cases of deeply nested structures, unpredictable query patterns, or
|
498
|
+
methods that take a long time to run, caching can improve performance.
|
499
|
+
|
500
|
+
To enable caching, use `cached`, which calls `cache_key` in the object:
|
501
|
+
|
502
|
+
```ruby
|
503
|
+
class CachedUserSerializer < UserSerializer
|
504
|
+
cached
|
505
|
+
end
|
506
|
+
```
|
507
|
+
|
508
|
+
You can also provide a lambda to `cached_with_key` to define a custom key:
|
509
|
+
|
510
|
+
```ruby
|
511
|
+
class CachedUserSerializer < UserSerializer
|
512
|
+
cached_with_key ->(user) {
|
513
|
+
"#{ user.id }/#{ user.current_sign_in_at }"
|
514
|
+
}
|
515
|
+
end
|
516
|
+
```
|
517
|
+
|
518
|
+
It will leverage `fetch_multi` when serializing a collection with `many` or
|
519
|
+
`has_many`, to minimize the amount of round trips needed to read and write all
|
520
|
+
items to cache.
|
521
|
+
|
522
|
+
This works specially well if your cache store also supports `write_multi`.
|
523
|
+
|
524
|
+
### Writing to JSON
|
525
|
+
|
526
|
+
In some corner cases it might be faster to serialize using a `Oj::StringWriter`,
|
527
|
+
which you can access by using `one_as_json` and `many_as_json`.
|
528
|
+
|
529
|
+
Alternatively, you can toggle this mode at a serializer level by using
|
530
|
+
`default_format :json`, or configure it globally from your base serializer:
|
531
|
+
|
532
|
+
```ruby
|
533
|
+
class BaseSerializer < JsonSerializer
|
534
|
+
default_format :json
|
535
|
+
end
|
536
|
+
```
|
537
|
+
|
538
|
+
This will change the default shortcuts (`render`, `one`, `one_if`, and `many`),
|
539
|
+
so that the serializer writes directly to JSON instead of returning a Hash.
|
540
|
+
|
541
|
+
Even when using this mode, you can still use rendered values inside arrays,
|
542
|
+
hashes, and other serializers, thanks to [the `raw_json` extensions][raw_json].
|
543
|
+
|
544
|
+
<details>
|
545
|
+
<summary>Example Output</summary>
|
546
|
+
|
547
|
+
```json
|
548
|
+
{
|
549
|
+
"name": "Abraxas",
|
550
|
+
"genres": [
|
551
|
+
"Pyschodelic Rock",
|
552
|
+
"Blues Rock",
|
553
|
+
"Jazz Fusion",
|
554
|
+
"Latin Rock"
|
555
|
+
],
|
556
|
+
"release": "September 23, 1970",
|
557
|
+
"songs": [
|
558
|
+
{
|
559
|
+
"track": 1,
|
560
|
+
"name": "Sing Winds, Crying Beasts",
|
561
|
+
"composers": [
|
562
|
+
"Michael Carabello"
|
563
|
+
]
|
564
|
+
},
|
565
|
+
{
|
566
|
+
"track": 2,
|
567
|
+
"name": "Black Magic Woman / Gypsy Queen",
|
568
|
+
"composers": [
|
569
|
+
"Peter Green",
|
570
|
+
"Gábor Szabó"
|
571
|
+
]
|
572
|
+
},
|
573
|
+
{
|
574
|
+
"track": 3,
|
575
|
+
"name": "Oye como va",
|
576
|
+
"composers": [
|
577
|
+
"Tito Puente"
|
578
|
+
]
|
579
|
+
},
|
580
|
+
{
|
581
|
+
"track": 4,
|
582
|
+
"name": "Incident at Neshabur",
|
583
|
+
"composers": [
|
584
|
+
"Alberto Gianquinto",
|
585
|
+
"Carlos Santana"
|
586
|
+
]
|
587
|
+
},
|
588
|
+
{
|
589
|
+
"track": 5,
|
590
|
+
"name": "Se acabó",
|
591
|
+
"composers": [
|
592
|
+
"José Areas"
|
593
|
+
]
|
594
|
+
},
|
595
|
+
{
|
596
|
+
"track": 6,
|
597
|
+
"name": "Mother's Daughter",
|
598
|
+
"composers": [
|
599
|
+
"Gregg Rolie"
|
600
|
+
]
|
601
|
+
},
|
602
|
+
{
|
603
|
+
"track": 7,
|
604
|
+
"name": "Samba pa ti",
|
605
|
+
"composers": [
|
606
|
+
"Santana"
|
607
|
+
]
|
608
|
+
},
|
609
|
+
{
|
610
|
+
"track": 8,
|
611
|
+
"name": "Hope You're Feeling Better",
|
612
|
+
"composers": [
|
613
|
+
"Rolie"
|
614
|
+
]
|
615
|
+
},
|
616
|
+
{
|
617
|
+
"track": 9,
|
618
|
+
"name": "El Nicoya",
|
619
|
+
"composers": [
|
620
|
+
"Areas"
|
621
|
+
]
|
622
|
+
}
|
623
|
+
]
|
624
|
+
}
|
625
|
+
```
|
626
|
+
</details>
|
627
|
+
|
628
|
+
## Design 📐
|
629
|
+
|
630
|
+
Unlike `ActiveModel::Serializer`, which builds a Hash that then gets encoded to
|
631
|
+
JSON, this implementation can use `Oj::StringWriter` to write JSON directly,
|
632
|
+
greatly reducing the overhead of allocating and garbage collecting the hashes.
|
633
|
+
|
634
|
+
It also allocates a single instance per serializer class, which makes it easy
|
635
|
+
to use, while keeping memory usage under control.
|
636
|
+
|
637
|
+
The internal design is simple and extensible, and because the library is written
|
638
|
+
in Ruby, creating new serialization strategies requires very little code.
|
639
|
+
Please open a [Discussion] if you need help 😃
|
640
|
+
|
641
|
+
### Comparison with other libraries
|
642
|
+
|
643
|
+
`ActiveModel::Serializer` instantiates one serializer object per item to be serialized.
|
644
|
+
|
645
|
+
Other libraries such as [`blueprinter`][blueprinter] [`jsonapi-serializer`][jsonapi]
|
646
|
+
evaluate serializers in the context of a `class` instead of an `instance` of a class.
|
647
|
+
The downside is that you can't use instance methods or local memoization, and any
|
648
|
+
mixins must be applied to the class itself.
|
649
|
+
|
650
|
+
[`panko-serializer`][panko] also uses `Oj::StringWriter`, but it has the big downside of having to own the entire render tree. Putting a serializer inside a Hash or an Active Model Serializer and serializing that to JSON doesn't work, making a gradual migration harder to achieve. Also, it's optimized for Active Record but I needed good Mongoid support.
|
651
|
+
|
652
|
+
`JsonSerializer` combines some of these ideas, by using instances, but reusing them to avoid object allocations. Serializing 10,000 items instantiates a single serializer. Unlike `panko-serializer`, it doesn't suffer from [double encoding problems](https://panko.dev/docs/response-bag) so it's easier to use.
|
653
|
+
|
654
|
+
Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
|
655
|
+
|
656
|
+
As a result, migrating from `active_model_serializers` is relatively
|
657
|
+
straightforward because instance methods, inheritance, and mixins work as usual.
|
658
|
+
|
659
|
+
### Benchmarks 📊
|
660
|
+
|
661
|
+
This library includes some [benchmarks] to compare performance with similar libraries.
|
662
|
+
|
663
|
+
See [this pull request](https://github.com/ElMassimo/oj_serializers/pull/9) for a quick comparison,
|
664
|
+
or check the CI to see the latest results.
|
665
|
+
|
666
|
+
### Migrating from other libraries
|
667
|
+
|
668
|
+
Please refer to the [migration guide] for a full discussion of the compatibility
|
669
|
+
modes available to make it easier to migrate from `active_model_serializers` and
|
670
|
+
similar libraries.
|
671
|
+
|
672
|
+
## Formatting 📏
|
673
|
+
|
674
|
+
Even though most of the examples above use a single-line style to be succint, I highly recommend writing one attribute per line, sorting them alphabetically (most editors can do it for you), and [always using a trailing comma][trailing_commas].
|
675
|
+
|
676
|
+
```ruby
|
677
|
+
class AlbumSerializer < JsonSerializer
|
678
|
+
attributes(
|
679
|
+
:genres,
|
680
|
+
:name,
|
681
|
+
:release_date,
|
682
|
+
)
|
683
|
+
end
|
684
|
+
```
|
685
|
+
|
686
|
+
It will make things clearer, minimize the amount of git conflicts, and keep the history a lot cleaner and more meaningful when using `git blame`.
|
687
|
+
|
688
|
+
## Special Thanks 🙏
|
689
|
+
|
690
|
+
This library wouldn't be possible without the wonderful and performant [`oj`](https://github.com/ohler55/oj) library. Thanks [Peter](https://github.com/ohler55)! 😃
|
691
|
+
|
692
|
+
Also, thanks to the libraries that inspired this one:
|
693
|
+
|
694
|
+
- [`active_model_serializers`][ams]: For the DSL
|
695
|
+
- [`panko-serializer`][panko]: For validating that using `Oj::StringWriter` was indeed fast
|
696
|
+
|
697
|
+
## License
|
698
|
+
|
699
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|