oj_serializers 2.0.0.pre.beta.1 → 2.0.0
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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +226 -140
- data/lib/oj_serializers/serializer.rb +153 -49
- data/lib/oj_serializers/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 281e961cd6a9da8c3723dd56ce7e1aa8ec112c57d13ab15e763ecef98eb1f659
|
4
|
+
data.tar.gz: cd9a42db1ea64aac9c75755550ce0bb3123646ac85020056a4d3bc5dae7e9251
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3816ef19190cf4dbddfa942d205b21cb0d639ed33cf9ff34a9972038179d677cfabe659fc36f1780e6fb64fb8c7145a756012bfc1a8de3bc4cb0256e419917a
|
7
|
+
data.tar.gz: 364da01e189d7c00b4abcbaa490c2e8a0432acf9f126a0db67ccab3d1a2c7a1b1724267ec084980ba706ad43fb89c1964749a4684736d72888f6ddb5fe970f3f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
## [Oj Serializers 2.0.0 (2023-03-27)](https://github.com/ElMassimo/oj_serializers/pull/9)
|
2
|
+
|
3
|
+
### Features ✨
|
4
|
+
|
5
|
+
- Improved performance (20% to 40% faster than v1)
|
6
|
+
- Added `render_as_hash` to efficiently build a Hash from the serializer
|
7
|
+
- `transform_keys :camelize`: a built-in setting to convert keys, in a way that does not affect runtime performance
|
8
|
+
- `sort_keys_by :name`: allows to sort the response alphabetically, without affecting runtime performance
|
9
|
+
- `render` shortcut, unifying `one` and `many`
|
10
|
+
- `attribute` as an easier approach to define serializer attributes
|
11
|
+
|
12
|
+
### Breaking Changes
|
13
|
+
|
14
|
+
Since returning a `Hash` is more convenient than returning a `Oj::StringWriter`, and performance is comparable, `default_format :hash` is now the default.
|
15
|
+
|
16
|
+
The previous APIs will still be available as `one_as_json` and `many_as_json`, as well as `default_format :json` to make the library work like in version 1.
|
17
|
+
|
1
18
|
## Oj Serializers 1.0.2 (2023-03-01) ##
|
2
19
|
|
3
20
|
* [fix: avoid freezing `ALLOWED_INSTANCE_VARIABLES`](https://github.com/ElMassimo/oj_serializers/commit/ade0302)
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ Oj Serializers
|
|
6
6
|
<a href="https://codeclimate.com/github/ElMassimo/oj_serializers"><img alt="Maintainability" src="https://codeclimate.com/github/ElMassimo/oj_serializers/badges/gpa.svg"/></a>
|
7
7
|
<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>
|
8
8
|
<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>
|
9
|
-
<a href="https://github.com/ElMassimo/oj_serializers/blob/
|
9
|
+
<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>
|
10
10
|
</p>
|
11
11
|
</h1>
|
12
12
|
|
@@ -19,14 +19,15 @@ Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] librar
|
|
19
19
|
[panko]: https://github.com/panko-serializer/panko_serializer
|
20
20
|
[blueprinter]: https://github.com/procore/blueprinter
|
21
21
|
[benchmarks]: https://github.com/ElMassimo/oj_serializers/tree/master/benchmarks
|
22
|
-
[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/
|
23
|
-
[sugar]: https://github.com/ElMassimo/oj_serializers/blob/
|
24
|
-
[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/
|
22
|
+
[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/main/benchmarks/document_benchmark.rb
|
23
|
+
[sugar]: https://github.com/ElMassimo/oj_serializers/blob/main/lib/oj_serializers/sugar.rb#L14
|
24
|
+
[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/main/MIGRATION_GUIDE.md
|
25
25
|
[design]: https://github.com/ElMassimo/oj_serializers#design-
|
26
26
|
[raw_json]: https://github.com/ohler55/oj/issues/542
|
27
27
|
[trailing_commas]: https://maximomussini.com/posts/trailing-commas/
|
28
28
|
[render dsl]: https://github.com/ElMassimo/oj_serializers#render-dsl-
|
29
29
|
[sorbet]: https://sorbet.org/
|
30
|
+
[Discussion]: https://github.com/ElMassimo/oj_serializers/discussions
|
30
31
|
|
31
32
|
## Why? 🤔
|
32
33
|
|
@@ -39,8 +40,8 @@ Learn more about [how this library achieves its performance][design].
|
|
39
40
|
|
40
41
|
## Features ⚡️
|
41
42
|
|
42
|
-
- Declaration syntax similar to Active Model Serializers
|
43
|
-
- Reduced memory allocation and [improved performance][benchmarks]
|
43
|
+
- Declaration syntax [similar to Active Model Serializers][migration guide]
|
44
|
+
- Reduced [memory allocation][benchmarks] and [improved performance][benchmarks]
|
44
45
|
- Support for `has_one` and `has_many`, compose with `flat_one`
|
45
46
|
- Useful development checks to avoid typos and mistakes
|
46
47
|
- Integrates nicely with Rails controllers
|
@@ -66,7 +67,7 @@ attributes should be serialized.
|
|
66
67
|
class AlbumSerializer < Oj::Serializer
|
67
68
|
attributes :name, :genres
|
68
69
|
|
69
|
-
|
70
|
+
attr
|
70
71
|
def release
|
71
72
|
album.release_date.strftime('%B %d, %Y')
|
72
73
|
end
|
@@ -139,48 +140,23 @@ end
|
|
139
140
|
```
|
140
141
|
</details>
|
141
142
|
|
142
|
-
|
143
|
-
|
144
|
-
To use the serializer, the recommended approach is:
|
143
|
+
You can then use your new serializer to render an object or collection:
|
145
144
|
|
146
145
|
```ruby
|
147
146
|
class AlbumsController < ApplicationController
|
148
147
|
def show
|
149
|
-
album = Album.find(params[:id])
|
150
148
|
render json: AlbumSerializer.one(album)
|
151
149
|
end
|
152
150
|
|
153
151
|
def index
|
154
|
-
albums = Album.all
|
155
152
|
render json: { albums: AlbumSerializer.many(albums) }
|
156
153
|
end
|
157
154
|
end
|
158
155
|
```
|
159
156
|
|
160
|
-
|
161
|
-
|
162
|
-
```ruby
|
163
|
-
require 'oj_serializers/sugar'
|
164
|
-
|
165
|
-
class AlbumsController < ApplicationController
|
166
|
-
def show
|
167
|
-
album = Album.find(params[:id])
|
168
|
-
render json: album, serializer: AlbumSerializer
|
169
|
-
end
|
170
|
-
|
171
|
-
def index
|
172
|
-
albums = Album.all
|
173
|
-
render json: albums, each_serializer: AlbumSerializer, root: :albums
|
174
|
-
end
|
175
|
-
end
|
176
|
-
```
|
177
|
-
|
178
|
-
It's recommended to create your own `BaseSerializer` class in order to easily
|
179
|
-
add custom extensions, specially when migrating from `active_model_serializers`.
|
157
|
+
## Rendering 🖨
|
180
158
|
|
181
|
-
|
182
|
-
|
183
|
-
In order to efficiently reuse the instances, serializers can't be instantiated directly. Use `one` and `many` to serialize objects or enumerables:
|
159
|
+
Use `one` to serialize objects, and `many` to serialize enumerables:
|
184
160
|
|
185
161
|
```ruby
|
186
162
|
render json: {
|
@@ -189,187 +165,212 @@ render json: {
|
|
189
165
|
}
|
190
166
|
```
|
191
167
|
|
192
|
-
|
193
|
-
|
194
|
-
|
168
|
+
Serializers can be rendered arrays, hashes, or even inside `ActiveModel::Serializer`
|
169
|
+
by using a method in the serializer, making it very easy to combine with other
|
170
|
+
libraries and migrate incrementally.
|
195
171
|
|
196
|
-
|
197
|
-
to JSON. Each method provides a different strategy to obtain the values to serialize.
|
172
|
+
You can use `render` as a shortcut for `one` and `many`, but it might be less readable:
|
198
173
|
|
199
|
-
|
200
|
-
|
174
|
+
```ruby
|
175
|
+
render json: {
|
176
|
+
favorite_album: AlbumSerializer.render(album),
|
177
|
+
purchased_albums: AlbumSerializer.render(albums),
|
178
|
+
}
|
179
|
+
```
|
201
180
|
|
202
|
-
|
181
|
+
## Attributes DSL 🪄
|
203
182
|
|
204
|
-
|
183
|
+
Specify which attributes should be rendered by calling a method in the object to serialize.
|
205
184
|
|
206
185
|
```ruby
|
207
186
|
class PlayerSerializer < Oj::Serializer
|
208
|
-
attributes :full_name
|
187
|
+
attributes :first_name, :last_name, :full_name
|
209
188
|
end
|
210
189
|
```
|
211
190
|
|
212
|
-
|
213
|
-
account methods defined in the serializer. Being explicit about where the
|
214
|
-
attribute is coming from makes the serializers easier to understand and more
|
215
|
-
maintainable.
|
216
|
-
|
217
|
-
### `serializer_attributes`
|
218
|
-
|
219
|
-
Obtains the attribute value by calling a method defined in the serializer.
|
220
|
-
|
221
|
-
Simply call `serialize` right before defining the method, and it will be serialized:
|
191
|
+
You can serialize custom values by specifying that a method is an `attribute`:
|
222
192
|
|
223
193
|
```ruby
|
224
194
|
class PlayerSerializer < Oj::Serializer
|
225
|
-
|
226
|
-
def full_name
|
195
|
+
attribute :name do
|
227
196
|
"#{player.first_name} #{player.last_name}"
|
228
197
|
end
|
229
|
-
end
|
230
|
-
```
|
231
|
-
|
232
|
-
> This inline syntax was inspired by how types are defined in [`sorbet`][sorbet].
|
233
198
|
|
234
|
-
|
235
|
-
`Serializer` suffix, `player` in the example above, or directly as `@object`.
|
199
|
+
# or
|
236
200
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
Works like `attributes` in Active Model Serializers, by calling a method in the serializer if defined, or calling `read_attribute_for_serialization` in the model.
|
242
|
-
|
243
|
-
```ruby
|
244
|
-
class AlbumSerializer < Oj::Serializer
|
245
|
-
ams_attributes :name, :release
|
246
|
-
|
247
|
-
def release
|
248
|
-
album.release_date.strftime('%B %d, %Y')
|
201
|
+
attribute
|
202
|
+
def name
|
203
|
+
"#{player.first_name} #{player.last_name}"
|
249
204
|
end
|
250
205
|
end
|
251
206
|
```
|
252
207
|
|
253
|
-
|
208
|
+
> **Note**
|
209
|
+
>
|
210
|
+
> In this example, `player` was inferred from `PlayerSerializer`.
|
211
|
+
>
|
212
|
+
> You can customize this by using [`object_as`](#using-a-different-alias-for-the-internal-object).
|
254
213
|
|
255
|
-
Instead, use `attributes` for model methods, and the inline `attribute` for serializer attributes. Being explicit makes serializers easier to understand, and to maintain.
|
256
214
|
|
257
|
-
|
215
|
+
### Associations 🔗
|
258
216
|
|
259
|
-
|
217
|
+
Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
|
260
218
|
|
261
|
-
|
219
|
+
You must specificy which serializer to use with the `serializer` option.
|
262
220
|
|
263
221
|
```ruby
|
264
|
-
class
|
265
|
-
|
222
|
+
class SongSerializer < Oj::Serializer
|
223
|
+
has_one :album, serializer: AlbumSerializer
|
224
|
+
has_many :composers, serializer: ComposerSerializer
|
266
225
|
end
|
267
|
-
|
268
|
-
PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
|
269
|
-
# {"first_name":"Mary","last_name":"Watson"}
|
270
226
|
```
|
271
227
|
|
272
|
-
|
273
|
-
|
274
|
-
Reads data directly from `attributes` in a [Mongoid] document.
|
275
|
-
|
276
|
-
By skipping type casting, coercion, and defaults, it [achieves the best performance][raw_benchmarks].
|
277
|
-
|
278
|
-
Although there are some downsides, depending on how consistent your schema is,
|
279
|
-
and which kind of consumer the API has, it can be really powerful.
|
228
|
+
Specify a different value for the association by providing a block:
|
280
229
|
|
281
230
|
```ruby
|
282
|
-
class
|
283
|
-
|
231
|
+
class SongSerializer < Oj::Serializer
|
232
|
+
has_one :album, serializer: AlbumSerializer do
|
233
|
+
Album.find_by(song_ids: song.id)
|
234
|
+
end
|
284
235
|
end
|
285
236
|
```
|
286
237
|
|
287
|
-
|
238
|
+
In case you need to pass options, you can call the serializer manually:
|
288
239
|
|
289
|
-
|
240
|
+
```ruby
|
241
|
+
class SongSerializer < Oj::Serializer
|
242
|
+
attribute :album do
|
243
|
+
AlbumSerializer.one(song.album, for_song: song)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
```
|
290
247
|
|
291
|
-
|
292
|
-
or by calling the method in the object being serialized.
|
248
|
+
### Aliasing or renaming attributes ↔️
|
293
249
|
|
294
|
-
You
|
250
|
+
You can pass `as` when defining an attribute or association to serialize it
|
251
|
+
using a different key:
|
295
252
|
|
296
253
|
```ruby
|
297
254
|
class SongSerializer < Oj::Serializer
|
298
|
-
has_one :album, serializer: AlbumSerializer
|
299
|
-
has_many :composers, serializer: ComposerSerializer
|
255
|
+
has_one :album, as: :first_release, serializer: AlbumSerializer
|
300
256
|
|
301
|
-
|
302
|
-
|
257
|
+
attributes title: {as: :name}
|
258
|
+
|
259
|
+
# or as a shortcut
|
260
|
+
attributes title: :name
|
303
261
|
end
|
304
262
|
```
|
305
263
|
|
306
|
-
|
264
|
+
### Conditional attributes ❔
|
265
|
+
|
266
|
+
You can render attributes and associations conditionally by using `:if`.
|
307
267
|
|
308
268
|
```ruby
|
309
|
-
class
|
310
|
-
|
311
|
-
def album
|
312
|
-
AlbumSerializer.one(song.album)
|
313
|
-
end
|
269
|
+
class PlayerSerializer < Oj::Serializer
|
270
|
+
attributes :first_name, :last_name, if: -> { player.display_name? }
|
314
271
|
|
315
|
-
|
316
|
-
def composers
|
317
|
-
ComposerSerializer.many(song.composers)
|
318
|
-
end
|
272
|
+
has_one :album, serializer: AlbumSerializer, if: -> { player.album }
|
319
273
|
end
|
320
274
|
```
|
321
275
|
|
322
|
-
|
276
|
+
This is useful in cases where you don't want to `null` values to be in the response.
|
277
|
+
|
278
|
+
## Advanced Usage 🧙♂️
|
323
279
|
|
324
280
|
### Using a different alias for the internal object
|
325
281
|
|
326
|
-
|
282
|
+
In most cases, the default alias for the `object` will be convenient enough.
|
283
|
+
|
284
|
+
However, if you would like to specify it manually, use `object_as`:
|
327
285
|
|
328
286
|
```ruby
|
329
287
|
class DiscographySerializer < Oj::Serializer
|
330
288
|
object_as :artist
|
331
289
|
|
332
290
|
# Now we can use `artist` instead of `object` or `discography`.
|
333
|
-
|
291
|
+
attribute
|
334
292
|
def latest_albums
|
335
293
|
artist.albums.desc(:year)
|
336
294
|
end
|
337
295
|
end
|
338
296
|
```
|
339
297
|
|
340
|
-
###
|
298
|
+
### Identifier attributes
|
341
299
|
|
342
|
-
|
343
|
-
|
300
|
+
The `identifier` method allows you to only include an identifier if the record
|
301
|
+
or document has been persisted.
|
344
302
|
|
345
303
|
```ruby
|
346
|
-
class
|
347
|
-
|
304
|
+
class AlbumSerializer < Oj::Serializer
|
305
|
+
identifier
|
348
306
|
|
349
|
-
|
307
|
+
# or if it's a different field
|
308
|
+
identifier :uuid
|
350
309
|
end
|
351
310
|
```
|
352
311
|
|
353
|
-
|
312
|
+
Additionally, identifier fields are always rendered first, even when sorting
|
313
|
+
fields alphabetically.
|
314
|
+
|
315
|
+
### Transforming attribute keys 🗝
|
316
|
+
|
317
|
+
When serialized data will be consumed from a client language that has different
|
318
|
+
naming conventions, it can be convenient to transform keys accordingly.
|
319
|
+
|
320
|
+
For example, when rendering an API to be consumed from the browser via JavaScript,
|
321
|
+
where properties are traditionally named using camel case.
|
354
322
|
|
355
|
-
|
323
|
+
Use `transform_keys` to handle that conversion.
|
356
324
|
|
357
325
|
```ruby
|
358
|
-
class
|
359
|
-
|
326
|
+
class BaseSerializer < Oj::Serializer
|
327
|
+
transform_keys :camelize
|
328
|
+
|
329
|
+
# shortcut for
|
330
|
+
transform_keys -> (key) { key.to_s.camelize(:lower) }
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
This has no performance impact, as keys will be transformed at load time.
|
335
|
+
|
336
|
+
### Sorting attributes 📶
|
337
|
+
|
338
|
+
By default attributes are rendered in the order they are defined.
|
339
|
+
|
340
|
+
If you would like to sort attributes alphabetically, you can specify it at a
|
341
|
+
serializer level:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
class BaseSerializer < Oj::Serializer
|
345
|
+
sort_attributes_by :name # or a Proc
|
346
|
+
end
|
347
|
+
```
|
348
|
+
|
349
|
+
This has no performance impact, as attributes will be sorted at load time.
|
360
350
|
|
361
|
-
|
351
|
+
### Path helpers 🛣
|
362
352
|
|
363
|
-
|
364
|
-
|
365
|
-
|
353
|
+
In case you need to access path helpers in your serializers, you can use the
|
354
|
+
following:
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
class BaseSerializer < Oj::Serializer
|
358
|
+
include Rails.application.routes.url_helpers
|
359
|
+
|
360
|
+
def default_url_options
|
361
|
+
Rails.application.routes.default_url_options
|
366
362
|
end
|
367
363
|
end
|
368
364
|
```
|
369
365
|
|
370
|
-
|
366
|
+
One slight variation that might make it easier to maintain in the long term is
|
367
|
+
to use a separate singleton service to provide the url helpers and options, and
|
368
|
+
make it available as `urls`.
|
369
|
+
|
370
|
+
### Memoization & local state
|
371
371
|
|
372
|
-
Serializers are designed to be stateless so that an instanced can be reused, but
|
372
|
+
Serializers are designed to be stateless so that an instanced can be reused, but
|
373
|
+
sometimes it's convenient to store intermediate calculations.
|
373
374
|
|
374
375
|
Use `memo` for memoization and storing temporary information.
|
375
376
|
|
@@ -377,7 +378,7 @@ Use `memo` for memoization and storing temporary information.
|
|
377
378
|
class DownloadSerializer < Oj::Serializer
|
378
379
|
attributes :filename, :size
|
379
380
|
|
380
|
-
|
381
|
+
attribute
|
381
382
|
def progress
|
382
383
|
"#{ last_event&.progress || 0 }%"
|
383
384
|
end
|
@@ -392,23 +393,86 @@ private
|
|
392
393
|
end
|
393
394
|
```
|
394
395
|
|
395
|
-
###
|
396
|
+
### `hash_attributes` 🚀
|
396
397
|
|
397
|
-
|
398
|
+
Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator.
|
398
399
|
|
399
|
-
|
400
|
-
|
400
|
+
```ruby
|
401
|
+
class PersonSerializer < Oj::Serializer
|
402
|
+
hash_attributes 'first_name', :last_name
|
403
|
+
end
|
404
|
+
|
405
|
+
PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
|
406
|
+
# {first_name: "Mary", last_name: "Watson"}
|
407
|
+
```
|
408
|
+
|
409
|
+
### `mongo_attributes` 🚀
|
410
|
+
|
411
|
+
Reads data directly from `attributes` in a [Mongoid] document.
|
412
|
+
|
413
|
+
By skipping type casting, coercion, and defaults, it [achieves the best performance][raw_benchmarks].
|
414
|
+
|
415
|
+
Although there are some downsides, depending on how consistent your schema is,
|
416
|
+
and which kind of consumer the API has, it can be really powerful.
|
417
|
+
|
418
|
+
```ruby
|
419
|
+
class AlbumSerializer < Oj::Serializer
|
420
|
+
mongo_attributes :id, :name
|
421
|
+
end
|
422
|
+
```
|
423
|
+
|
424
|
+
### Caching 📦
|
425
|
+
|
426
|
+
Usually rendering is so fast that __turning caching on can be slower__.
|
427
|
+
|
428
|
+
However, in cases of deeply nested structures, unpredictable query patterns, or
|
429
|
+
methods that take a long time to run, caching can improve performance.
|
430
|
+
|
431
|
+
To enable caching, use `cached`, which calls `cache_key` in the object:
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
class CachedUserSerializer < UserSerializer
|
435
|
+
cached
|
436
|
+
end
|
437
|
+
```
|
438
|
+
|
439
|
+
You can also provide a lambda to `cached_with_key` to define a custom key:
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
class CachedUserSerializer < UserSerializer
|
443
|
+
cached_with_key ->(user) {
|
444
|
+
"#{ user.id }/#{ user.current_sign_in_at }"
|
445
|
+
}
|
446
|
+
end
|
447
|
+
```
|
448
|
+
|
449
|
+
It will leverage `fetch_multi` when serializing a collection with `many` or
|
450
|
+
`has_many`, to minimize the amount of round trips needed to read and write all
|
451
|
+
items to cache.
|
452
|
+
|
453
|
+
This works specially well if your cache store also supports `write_multi`.
|
454
|
+
|
455
|
+
### Writing to JSON
|
456
|
+
|
457
|
+
In some corner cases it might be faster to serialize using a `Oj::StringWriter`,
|
458
|
+
which you can access by using `one_as_json` and `many_as_json`.
|
459
|
+
|
460
|
+
Alternatively, you can toggle this mode at a serializer level by using
|
461
|
+
`default_format :json`, or configure it globally from your base serializer:
|
401
462
|
|
402
463
|
```ruby
|
403
464
|
class BaseSerializer < Oj::Serializer
|
404
|
-
default_format :json
|
465
|
+
default_format :json
|
405
466
|
end
|
406
467
|
```
|
407
468
|
|
408
469
|
This will change the default shortcuts (`render`, `one`, `one_if`, and `many`),
|
409
470
|
so that the serializer writes directly to JSON instead of returning a Hash.
|
410
471
|
|
411
|
-
|
472
|
+
> **Note**
|
473
|
+
>
|
474
|
+
> This was the default behavior in `oj_serializers` v1, but was replaced with
|
475
|
+
`default_format :hash` in v2.
|
412
476
|
|
413
477
|
<details>
|
414
478
|
<summary>Example Output</summary>
|
@@ -494,6 +558,9 @@ Follow [this discussion][raw_json] to find out more about [the `raw_json` extens
|
|
494
558
|
```
|
495
559
|
</details>
|
496
560
|
|
561
|
+
Even when using this mode, you can still use rendered values inside arrays,
|
562
|
+
hashes, and other serializers, thanks to [the `raw_json` extensions][raw_json].
|
563
|
+
|
497
564
|
## Design 📐
|
498
565
|
|
499
566
|
Unlike `ActiveModel::Serializer`, which builds a Hash that then gets encoded to
|
@@ -503,6 +570,10 @@ greatly reducing the overhead of allocating and garbage collecting the hashes.
|
|
503
570
|
It also allocates a single instance per serializer class, which makes it easy
|
504
571
|
to use, while keeping memory usage under control.
|
505
572
|
|
573
|
+
The internal design is simple and extensible, and because the library is written
|
574
|
+
in Ruby, creating new serialization strategies requires very little code.
|
575
|
+
Please open a [Discussion] if you need help 😃
|
576
|
+
|
506
577
|
### Comparison with other libraries
|
507
578
|
|
508
579
|
`ActiveModel::Serializer` instantiates one serializer object per item to be serialized.
|
@@ -516,9 +587,24 @@ mixins must be applied to the class itself.
|
|
516
587
|
|
517
588
|
`Oj::Serializer` 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.
|
518
589
|
|
590
|
+
Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
|
591
|
+
|
519
592
|
As a result, migrating from `active_model_serializers` is relatively
|
520
593
|
straightforward because instance methods, inheritance, and mixins work as usual.
|
521
594
|
|
595
|
+
### Benchmarks 📊
|
596
|
+
|
597
|
+
This library includes some [benchmarks] to compare performance with similar libraries.
|
598
|
+
|
599
|
+
See [this pull request](https://github.com/ElMassimo/oj_serializers/pull/9) for a quick comparison,
|
600
|
+
or check the CI to see the latest results.
|
601
|
+
|
602
|
+
### Migrating from other libraries
|
603
|
+
|
604
|
+
Please refer to the [migration guide] for a full discussion of the compatibility
|
605
|
+
modes available to make it easier to migrate from `active_model_serializers` and
|
606
|
+
similar libraries.
|
607
|
+
|
522
608
|
## Formatting 📏
|
523
609
|
|
524
610
|
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].
|
@@ -43,7 +43,7 @@ class OjSerializers::Serializer
|
|
43
43
|
def _check_instance_variables
|
44
44
|
if instance_values.keys.any? { |key| !ALLOWED_INSTANCE_VARIABLES.include?(key) }
|
45
45
|
bad_keys = instance_values.keys.reject { |key| ALLOWED_INSTANCE_VARIABLES.include?(key) }
|
46
|
-
raise ArgumentError, "Serializer instances are reused so they must be stateless. Use `memo.fetch` for memoization purposes instead. Bad keys: #{bad_keys.join(',')}"
|
46
|
+
raise ArgumentError, "Serializer instances are reused so they must be stateless. Use `memo.fetch` for memoization purposes instead. Bad keys: #{bad_keys.join(',')} in #{self.class}"
|
47
47
|
end
|
48
48
|
end
|
49
49
|
end
|
@@ -86,7 +86,8 @@ protected
|
|
86
86
|
# Public: Allows the user to specify `default_format :json`, as a simple
|
87
87
|
# way to ensure that `.one` and `.many` work as in Version 1.
|
88
88
|
def default_format(value)
|
89
|
-
|
89
|
+
@_default_format = value
|
90
|
+
define_serialization_shortcuts
|
90
91
|
end
|
91
92
|
|
92
93
|
# Public: Allows to sort fields by name instead.
|
@@ -125,6 +126,14 @@ protected
|
|
125
126
|
# reuse the same serializer instance to serialize different objects.
|
126
127
|
delegate :write_one, :write_many, :write_to_json, to: :instance
|
127
128
|
|
129
|
+
# Internal: Keep a reference to the default `write_one` method so that we
|
130
|
+
# can use it inside cached overrides and benchmark tests.
|
131
|
+
alias_method :non_cached_write_one, :write_one
|
132
|
+
|
133
|
+
# Internal: Keep a reference to the default `write_many` method so that we
|
134
|
+
# can use it inside cached overrides and benchmark tests.
|
135
|
+
alias_method :non_cached_write_many, :write_many
|
136
|
+
|
128
137
|
# Helper: Serializes one or more items.
|
129
138
|
def render(item, options = nil)
|
130
139
|
many?(item) ? many(item, options) : one(item, options)
|
@@ -190,7 +199,7 @@ protected
|
|
190
199
|
# Internal: Will alias the object according to the name of the wrapper class.
|
191
200
|
def inherited(subclass)
|
192
201
|
object_alias = subclass.name.demodulize.chomp('Serializer').underscore
|
193
|
-
subclass.object_as(object_alias) unless method_defined?(object_alias)
|
202
|
+
subclass.object_as(object_alias) unless method_defined?(object_alias) || object_alias == 'base'
|
194
203
|
super
|
195
204
|
end
|
196
205
|
|
@@ -203,7 +212,88 @@ protected
|
|
203
212
|
|
204
213
|
protected
|
205
214
|
|
206
|
-
|
215
|
+
# Internal: Calculates the cache_key used to cache one serialized item.
|
216
|
+
def item_cache_key(item, cache_key_proc)
|
217
|
+
ActiveSupport::Cache.expand_cache_key(cache_key_proc.call(item))
|
218
|
+
end
|
219
|
+
|
220
|
+
# Public: Allows to define a cache key strategy for the serializer.
|
221
|
+
# Defaults to calling cache_key in the object if no key is provided.
|
222
|
+
#
|
223
|
+
# NOTE: Benchmark it, sometimes caching is actually SLOWER.
|
224
|
+
def cached(cache_key_proc = :cache_key.to_proc)
|
225
|
+
cache_options = { namespace: "#{name}#write_to_json", version: OjSerializers::VERSION }.freeze
|
226
|
+
cache_hash_options = { namespace: "#{name}#render_as_hash", version: OjSerializers::VERSION }.freeze
|
227
|
+
|
228
|
+
# Internal: Redefine `one_as_hash` to use the cache for the serialized hash.
|
229
|
+
define_singleton_method(:one_as_hash) do |item, options = nil|
|
230
|
+
CACHE.fetch(item_cache_key(item, cache_key_proc), cache_hash_options) do
|
231
|
+
instance.render_as_hash(item, options)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Internal: Redefine `many_as_hash` to use the cache for the serialized hash.
|
236
|
+
define_singleton_method(:many_as_hash) do |items, options = nil|
|
237
|
+
# We define a one-off method for the class to receive the entire object
|
238
|
+
# inside the `fetch_multi` block. Otherwise we would only get the cache
|
239
|
+
# key, and we would need to build a Hash to retrieve the object.
|
240
|
+
#
|
241
|
+
# NOTE: The assignment is important, as queries would return different
|
242
|
+
# objects when expanding with the splat in fetch_multi.
|
243
|
+
items = items.entries.each do |item|
|
244
|
+
item_key = item_cache_key(item, cache_key_proc)
|
245
|
+
item.define_singleton_method(:cache_key) { item_key }
|
246
|
+
end
|
247
|
+
|
248
|
+
# Fetch all items at once by leveraging `read_multi`.
|
249
|
+
#
|
250
|
+
# NOTE: Memcached does not support `write_multi`, if we switch the cache
|
251
|
+
# store to use Redis performance would improve a lot for this case.
|
252
|
+
CACHE.fetch_multi(*items, cache_hash_options) do |item|
|
253
|
+
instance.render_as_hash(item, options)
|
254
|
+
end.values
|
255
|
+
end
|
256
|
+
|
257
|
+
# Internal: Redefine `write_one` to use the cache for the serialized JSON.
|
258
|
+
define_singleton_method(:write_one) do |external_writer, item, options = nil|
|
259
|
+
cached_item = CACHE.fetch(item_cache_key(item, cache_key_proc), cache_options) do
|
260
|
+
writer = new_json_writer
|
261
|
+
non_cached_write_one(writer, item, options)
|
262
|
+
writer.to_json
|
263
|
+
end
|
264
|
+
external_writer.push_json("#{cached_item}\n") # Oj.dump expects a new line terminator.
|
265
|
+
end
|
266
|
+
|
267
|
+
# Internal: Redefine `write_many` to use fetch_multi from cache.
|
268
|
+
define_singleton_method(:write_many) do |external_writer, items, options = nil|
|
269
|
+
# We define a one-off method for the class to receive the entire object
|
270
|
+
# inside the `fetch_multi` block. Otherwise we would only get the cache
|
271
|
+
# key, and we would need to build a Hash to retrieve the object.
|
272
|
+
#
|
273
|
+
# NOTE: The assignment is important, as queries would return different
|
274
|
+
# objects when expanding with the splat in fetch_multi.
|
275
|
+
items = items.entries.each do |item|
|
276
|
+
item_key = item_cache_key(item, cache_key_proc)
|
277
|
+
item.define_singleton_method(:cache_key) { item_key }
|
278
|
+
end
|
279
|
+
|
280
|
+
# Fetch all items at once by leveraging `read_multi`.
|
281
|
+
#
|
282
|
+
# NOTE: Memcached does not support `write_multi`, if we switch the cache
|
283
|
+
# store to use Redis performance would improve a lot for this case.
|
284
|
+
cached_items = CACHE.fetch_multi(*items, cache_options) do |item|
|
285
|
+
writer = new_json_writer
|
286
|
+
non_cached_write_one(writer, item, options)
|
287
|
+
writer.to_json
|
288
|
+
end.values
|
289
|
+
external_writer.push_json("#{OjSerializers::JsonValue.array(cached_items)}\n") # Oj.dump expects a new line terminator.
|
290
|
+
end
|
291
|
+
|
292
|
+
define_serialization_shortcuts
|
293
|
+
end
|
294
|
+
alias_method :cached_with_key, :cached
|
295
|
+
|
296
|
+
def define_serialization_shortcuts(format = _default_format)
|
207
297
|
case format
|
208
298
|
when :json, :hash
|
209
299
|
singleton_class.alias_method :one, :"one_as_#{format}"
|
@@ -219,20 +309,27 @@ protected
|
|
219
309
|
end
|
220
310
|
|
221
311
|
# Public: Identifiers are always serialized first.
|
312
|
+
#
|
313
|
+
# NOTE: We skip the id for non-persisted documents, since it doesn't
|
314
|
+
# actually identify the document (it will change once it's persisted).
|
222
315
|
def identifier(name = :id, **options)
|
223
|
-
add_attribute(name,
|
316
|
+
add_attribute(name, attribute: :method, if: -> { !@object.new_record? }, **options, identifier: true)
|
224
317
|
end
|
225
318
|
|
226
319
|
# Public: Specify a collection of objects that should be serialized using
|
227
320
|
# the specified serializer.
|
228
|
-
def has_many(name, serializer:, root: name, as: root, **options)
|
321
|
+
def has_many(name, serializer:, root: name, as: root, **options, &block)
|
322
|
+
define_method(name, &block) if block
|
229
323
|
add_attribute(name, association: :many, as: as, serializer: serializer, **options)
|
230
324
|
end
|
231
325
|
|
232
326
|
# Public: Specify an object that should be serialized using the serializer.
|
233
|
-
def has_one(name, serializer:, root: name, as: root, **options)
|
327
|
+
def has_one(name, serializer:, root: name, as: root, **options, &block)
|
328
|
+
define_method(name, &block) if block
|
234
329
|
add_attribute(name, association: :one, as: as, serializer: serializer, **options)
|
235
330
|
end
|
331
|
+
# Alias: From a serializer perspective, the association type does not matter.
|
332
|
+
alias_method :belongs_to, :has_one
|
236
333
|
|
237
334
|
# Public: Specify an object that should be serialized using the serializer,
|
238
335
|
# but unlike `has_one`, this one will write the attributes directly without
|
@@ -255,47 +352,57 @@ protected
|
|
255
352
|
#
|
256
353
|
# See ./benchmarks/document_benchmark.rb
|
257
354
|
def mongo_attributes(*method_names, **options)
|
258
|
-
|
259
|
-
|
355
|
+
identifier(:_id, as: :id, attribute: :mongoid, **options) if method_names.delete(:id)
|
356
|
+
attributes(*method_names, **options, attribute: :mongoid)
|
260
357
|
end
|
261
358
|
|
262
359
|
# Public: Specify which attributes are going to be obtained by calling a
|
263
360
|
# method in the object.
|
264
|
-
def attributes(*method_names, **
|
265
|
-
|
361
|
+
def attributes(*method_names, **methods_with_options)
|
362
|
+
attr_options = methods_with_options.extract!(:if, :as, :attribute)
|
363
|
+
attr_options[:attribute] ||= :method
|
364
|
+
|
365
|
+
method_names.each do |name|
|
366
|
+
add_attribute(name, attr_options)
|
367
|
+
end
|
368
|
+
|
369
|
+
methods_with_options.each do |name, options|
|
370
|
+
options = { as: options } if options.is_a?(Symbol)
|
371
|
+
add_attribute(name, options)
|
372
|
+
end
|
266
373
|
end
|
267
|
-
alias_method :attribute, :attributes
|
268
374
|
|
269
375
|
# Public: Specify which attributes are going to be obtained by calling a
|
270
376
|
# method in the serializer.
|
271
|
-
#
|
272
|
-
# NOTE: This can be one of the slowest strategies, when in doubt, measure.
|
273
377
|
def serializer_attributes(*method_names, **options)
|
274
|
-
|
378
|
+
attributes(*method_names, **options, attribute: :serializer)
|
275
379
|
end
|
276
380
|
|
277
381
|
# Syntax Sugar: Allows to use it before a method name.
|
278
382
|
#
|
279
383
|
# Example:
|
280
|
-
#
|
384
|
+
# attribute
|
281
385
|
# def full_name
|
282
386
|
# "#{ first_name } #{ last_name }"
|
283
387
|
# end
|
284
|
-
def
|
388
|
+
def attribute(name = nil, **options, &block)
|
389
|
+
options[:attribute] = :serializer
|
285
390
|
if name
|
286
|
-
|
391
|
+
define_method(name, &block) if block
|
392
|
+
add_attribute(name, options)
|
287
393
|
else
|
288
|
-
@
|
394
|
+
@_current_attribute_options = options
|
289
395
|
end
|
290
396
|
end
|
397
|
+
alias_method :attr, :attribute
|
291
398
|
|
292
399
|
# Internal: Intercept a method definition, tying a type that was
|
293
400
|
# previously specified to the name of the attribute.
|
294
401
|
def method_added(name)
|
295
402
|
super(name)
|
296
|
-
if @
|
297
|
-
|
298
|
-
@
|
403
|
+
if @_current_attribute_options
|
404
|
+
add_attribute(name, @_current_attribute_options)
|
405
|
+
@_current_attribute_options = nil
|
299
406
|
end
|
300
407
|
end
|
301
408
|
|
@@ -307,7 +414,13 @@ protected
|
|
307
414
|
method_names.each do |method_name|
|
308
415
|
define_method(method_name) { @object.read_attribute_for_serialization(method_name) } unless method_defined?(method_name)
|
309
416
|
end
|
310
|
-
|
417
|
+
attributes(*method_names, **options, attribute: :serializer)
|
418
|
+
end
|
419
|
+
|
420
|
+
# Internal: The default format to use for `render`, `one`, and `many`.
|
421
|
+
def _default_format
|
422
|
+
@_default_format = superclass.try(:_default_format) || :hash unless defined?(@_default_format)
|
423
|
+
@_default_format
|
311
424
|
end
|
312
425
|
|
313
426
|
# Internal: The strategy to use when sorting the fields.
|
@@ -328,10 +441,6 @@ protected
|
|
328
441
|
|
329
442
|
private
|
330
443
|
|
331
|
-
def add_attributes(names, options)
|
332
|
-
names.each { |name| add_attribute(name, options) }
|
333
|
-
end
|
334
|
-
|
335
444
|
def add_attribute(name, options)
|
336
445
|
_attributes[name.to_s.freeze] = options
|
337
446
|
end
|
@@ -420,18 +529,27 @@ protected
|
|
420
529
|
RESCUE_NO_METHOD
|
421
530
|
end
|
422
531
|
|
532
|
+
# Internal: Detects any include methods defined in the serializer, or defines
|
533
|
+
# one by using the lambda passed in the `if` option, if any.
|
534
|
+
def check_conditional_method(method_name, options)
|
535
|
+
include_method_name = "include_#{method_name}#{'?' unless method_name.to_s.ends_with?('?')}"
|
536
|
+
if render_if = options[:if]
|
537
|
+
if render_if.is_a?(Symbol)
|
538
|
+
alias_method(include_method_name, render_if)
|
539
|
+
else
|
540
|
+
define_method(include_method_name, &render_if)
|
541
|
+
end
|
542
|
+
end
|
543
|
+
include_method_name if method_defined?(include_method_name)
|
544
|
+
end
|
545
|
+
|
423
546
|
# Internal: Returns the code to render an attribute or association
|
424
547
|
# conditionally.
|
425
548
|
#
|
426
549
|
# NOTE: Detects any include methods defined in the serializer, or defines
|
427
550
|
# one by using the lambda passed in the `if` option, if any.
|
428
551
|
def code_to_write_conditional(method_name, options)
|
429
|
-
include_method_name =
|
430
|
-
if render_if = options[:if]
|
431
|
-
define_method(include_method_name, &render_if)
|
432
|
-
end
|
433
|
-
|
434
|
-
if method_defined?(include_method_name)
|
552
|
+
if (include_method_name = check_conditional_method(method_name, options))
|
435
553
|
"if #{include_method_name};#{yield};end\n"
|
436
554
|
else
|
437
555
|
yield
|
@@ -455,12 +573,6 @@ protected
|
|
455
573
|
when :mongoid
|
456
574
|
# Writes an Mongoid attribute to JSON, this is the fastest strategy.
|
457
575
|
"writer.push_value(@object.attributes['#{method_name}'], #{key})"
|
458
|
-
when :id
|
459
|
-
# Writes an _id value to JSON using `id` as the key instead.
|
460
|
-
#
|
461
|
-
# NOTE: We skip the id for non-persisted documents, since it doesn't actually
|
462
|
-
# identify the document (it will change once it's persisted).
|
463
|
-
"writer.push_value(@object.attributes['_id'], 'id') unless @object.new_record?"
|
464
576
|
else
|
465
577
|
raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
|
466
578
|
end
|
@@ -501,13 +613,7 @@ protected
|
|
501
613
|
# NOTE: Detects any include methods defined in the serializer, or defines
|
502
614
|
# one by using the lambda passed in the `if` option, if any.
|
503
615
|
def code_to_render_conditionally(method_name, options)
|
504
|
-
include_method_name =
|
505
|
-
|
506
|
-
if render_if = options[:if]
|
507
|
-
define_method(include_method_name, &render_if)
|
508
|
-
end
|
509
|
-
|
510
|
-
if method_defined?(include_method_name)
|
616
|
+
if (include_method_name = check_conditional_method(method_name, options))
|
511
617
|
"**(#{include_method_name} ? {#{yield}} : {})"
|
512
618
|
else
|
513
619
|
yield
|
@@ -526,8 +632,6 @@ protected
|
|
526
632
|
"#{key}: @object[#{method_name.inspect}]"
|
527
633
|
when :mongoid
|
528
634
|
"#{key}: @object.attributes['#{method_name}']"
|
529
|
-
when :id
|
530
|
-
"**(@object.new_record? ? {} : {id: @object.attributes['_id']})"
|
531
635
|
else
|
532
636
|
raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
|
533
637
|
end
|
@@ -585,7 +689,7 @@ protected
|
|
585
689
|
end
|
586
690
|
end
|
587
691
|
|
588
|
-
define_serialization_shortcuts
|
692
|
+
define_serialization_shortcuts
|
589
693
|
end
|
590
694
|
|
591
695
|
Oj::Serializer = OjSerializers::Serializer unless defined?(Oj::Serializer)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: oj_serializers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.0
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maximo Mussini
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-03-
|
11
|
+
date: 2023-03-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -64,9 +64,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
64
|
version: 2.7.0
|
65
65
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- - "
|
67
|
+
- - ">="
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version:
|
69
|
+
version: '0'
|
70
70
|
requirements: []
|
71
71
|
rubygems_version: 3.2.32
|
72
72
|
signing_key:
|