diff --git a/.gitignore b/.gitignore index 8c4389d..5a0f241 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /pkg/ /spec/reports/ /tmp/ +/log/ /benchmark/flamegraphs/ /gemfiles/*.lock diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f837b9d..0830495 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -19,7 +19,7 @@ Bundler/OrderedGems: # Include: **/*.gemspec Gemspec/RequiredRubyVersion: Exclude: - - 'oj_serializers.gemspec' + - 'json_serializers.gemspec' # Offense count: 5 # This cop supports safe autocorrection (--autocorrect). @@ -44,21 +44,21 @@ Lint/RedundantDirGlobSort: # SupportedStyles: strict, consistent Lint/SymbolConversion: Exclude: - - 'lib/oj_serializers/serializer.rb' + - 'lib/json_serializers/serializer.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Exclude: - - 'lib/oj_serializers/serializer.rb' + - 'lib/json_serializers/serializer.rb' # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedReceivers. Lint/UselessDefaultValueArgument: Exclude: - - 'lib/oj_serializers/serializer.rb' + - 'lib/json_serializers/serializer.rb' # Offense count: 5 # This cop supports safe autocorrection (--autocorrect). @@ -72,7 +72,7 @@ Style/StringLiterals: # This cop supports safe autocorrection (--autocorrect). Style/SuperArguments: Exclude: - - 'lib/oj_serializers/serializer.rb' + - 'lib/json_serializers/serializer.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -88,7 +88,7 @@ Style/SymbolProc: # SupportedStylesForMultiline: comma, consistent_comma, no_comma Style/TrailingCommaInArguments: Exclude: - - 'spec/oj_serializers/sort_attributes_spec.rb' + - 'spec/json_serializers/sort_attributes_spec.rb' # Offense count: 17 # This cop supports safe autocorrection (--autocorrect). diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a191eb..b5fa011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,16 @@ -## Oj Serializers 3.0.0 (2026-01-02) +## JSON Serializers 3.0.0 (2026-01-02) ### Features ✨ - [Pass user options to children associations](https://github.com/ElMassimo/oj_serializers/commit/d495f06) -## Oj Serializers 2.1.0 (2026-01-02) +## JSON Serializers 2.1.0 (2026-01-02) ### Fixes 🐞 - [Improve sorting by :definition with more than 10 attributes in the list](https://github.com/ElMassimo/oj_serializers/commit/d58cb81) (#25) -## Oj Serializers 2.0.3 (2023-04-19) +## JSON Serializers 2.0.3 (2023-04-19) ### Features ✨ @@ -20,7 +20,7 @@ - [Allow using `active_model_serializers` in associations](https://github.com/ElMassimo/oj_serializers/commit/501ed4014b564e6f103d2f52d15832fe6706d6a8) -## Oj Serializers 2.0.2 (2023-04-02) +## JSON Serializers 2.0.2 (2023-04-02) ### Features ✨ @@ -30,7 +30,7 @@ - [Error when defining attributes with options](https://github.com/ElMassimo/oj_serializers/commit/680ab47) -## Oj Serializers 2.0.1 (2023-04-02) +## JSON Serializers 2.0.1 (2023-04-02) ### Features ✨ @@ -41,7 +41,7 @@ - [Aliased attributes should be sorted by the output key](https://github.com/ElMassimo/oj_serializers/commit/fc6f4c1) -## [Oj Serializers 2.0.0 (2023-03-27)](https://github.com/ElMassimo/oj_serializers/pull/9) +## [JSON Serializers 2.0.0 (2023-03-27)](https://github.com/ElMassimo/oj_serializers/pull/9) ### Features ✨ @@ -54,20 +54,18 @@ ### Breaking Changes -Since returning a `Hash` is more convenient than returning a `Oj::StringWriter`, and performance is comparable, `default_format :hash` is now the default. +Since returning a `Hash` is more convenient and performance is comparable, `default_format :hash` is now the default. -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. - -## Oj Serializers 1.0.2 (2023-03-01) ## +## JSON Serializers 1.0.2 (2023-03-01) ## * [fix: avoid freezing `ALLOWED_INSTANCE_VARIABLES`](https://github.com/ElMassimo/oj_serializers/commit/ade0302) -## Oj Serializers 1.0.1 (2023-03-01) ## +## JSON Serializers 1.0.1 (2023-03-01) ## * [fix: avoid caching instances of reloaded classes in development](https://github.com/ElMassimo/oj_serializers/commit/0bd928d64d159926acf6b4d57e3f08b12f6931ce) -## Oj Serializers 1.0.0 (2020-11-05) ## +## JSON Serializers 1.0.0 (2020-11-05) ## * Initial Release. diff --git a/Gemfile.lock b/Gemfile.lock index 6a1a1fe..d07dc29 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,7 @@ PATH remote: . specs: - oj_serializers (3.0.0) - oj (>= 3.14.0) + json_serializers (3.0.0) GEM remote: https://rubygems.org/ @@ -90,7 +89,7 @@ GEM benchmark-ips (2.14.0) benchmark-memory (0.2.0) memory_profiler (~> 1) - bigdecimal (3.2.2) + bigdecimal (4.0.1) blueprinter (0.30.0) bson (5.1.1) builder (3.3.0) @@ -116,7 +115,7 @@ GEM pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.12.2) + json (2.19.2) jsonapi-renderer (0.2.2) language_server-protocol (3.17.0.5) lint_roller (1.1.0) @@ -155,10 +154,10 @@ GEM racc (~> 1.4) nokogiri (1.18.8-x86_64-linux-gnu) racc (~> 1.4) - oj (3.16.11) + oj (3.16.16) bigdecimal (>= 3.0) ostruct (>= 0.2) - ostruct (0.6.2) + ostruct (0.6.3) panko_serializer (0.8.3) activesupport oj (> 3.11.0, < 4.0.0) @@ -296,9 +295,9 @@ DEPENDENCIES benchmark-memory blueprinter (~> 0.8) json + json_serializers! memory_profiler mongoid - oj_serializers! panko_serializer pry-byebug (~> 3.9) rails @@ -310,4 +309,4 @@ DEPENDENCIES sqlite3 BUNDLED WITH - 2.3.22 + 2.7.2 diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index b93440d..a35d200 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -5,17 +5,15 @@ [readme]: https://github.com/ElMassimo/oj_serializers/blob/main/README.md [attributes dsl]: https://github.com/ElMassimo/oj_serializers/blob/main/README.md#attributes-dsl- -[oj]: https://github.com/ohler55/oj [ams]: https://github.com/rails-api/active_model_serializers [jsonapi]: https://github.com/jsonapi-serializer/jsonapi-serializer [panko]: https://github.com/panko-serializer/panko_serializer [benchmarks]: https://github.com/ElMassimo/oj_serializers/tree/master/benchmarks [raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/main/benchmarks/document_benchmark.rb [migration guide]: https://github.com/ElMassimo/oj_serializers/blob/main/MIGRATION_GUIDE.md -[raw_json]: https://github.com/ohler55/oj/issues/542 [trailing_commas]: https://maximomussini.com/posts/trailing-commas/ -The DSL of `oj_serializers` is meant to be similar to the one provided by `active_model_serializers` to make the migration process simple, +The DSL of `json_serializers` is meant to be similar to the one provided by `active_model_serializers` to make the migration process simple, though the goal is not to be a drop-in replacement. ## Rendering πŸ›  @@ -24,7 +22,7 @@ To use the same format in controllers, using the `root`, `serializer`, `each_ser ```ruby # config/initializers/json.rb -require 'oj_serializers/compat' +require 'json_serializers/compat' ``` Otherwise, use `one` and `many` to serialize objects or enumerables: @@ -80,7 +78,7 @@ end # becomes -class AlbumSerializer < Oj::Serializer +class AlbumSerializer < JsonSerializer ams_attributes :name, :release # The serializer class must be explicitly provided. @@ -102,7 +100,7 @@ Once your serializer is working as expected, you can further refactor it to be m Being explicit about where the attributes are coming from makes the serializers easier to understand and more maintainable. ```ruby -class AlbumSerializer < Oj::Serializer +class AlbumSerializer < JsonSerializer attributes :name has_many :songs, serializer: SongSerializer @@ -140,7 +138,7 @@ In case you need to access path helpers in your serializers, you can use the following: ```ruby -class BaseJsonSerializer < Oj::Serializer +class BaseJsonSerializer < JsonSerializer include Rails.application.routes.url_helpers def default_url_options @@ -165,7 +163,7 @@ class ApplicationController < ActionController::Base before_action { Thread.current[:current_controller] = self } end -class BaseJsonSerializer < Oj::Serializer +class BaseJsonSerializer < JsonSerializer def scope @scope ||= Thread.current[:current_controller] end diff --git a/README.md b/README.md index e3177a5..2df1475 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@

-Oj Serializers +JSON Serializers

Build Status Maintainability Test Coverage -Gem Version +Gem Version License

-Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] library. +Fast, memory-efficient JSON serializers for Ruby and Rails. -[oj]: https://github.com/ohler55/oj [mongoid]: https://github.com/mongodb/mongoid [ams]: https://github.com/rails-api/active_model_serializers [jsonapi]: https://github.com/jsonapi-serializer/jsonapi-serializer @@ -19,12 +18,11 @@ Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] librar [blueprinter]: https://github.com/procore/blueprinter [benchmarks]: https://github.com/ElMassimo/oj_serializers/tree/master/benchmarks [raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/main/benchmarks/document_benchmark.rb -[sugar]: https://github.com/ElMassimo/oj_serializers/blob/main/lib/oj_serializers/sugar.rb#L14 +[sugar]: https://github.com/ElMassimo/oj_serializers/blob/main/lib/json_serializers/sugar.rb#L14 [migration guide]: https://github.com/ElMassimo/oj_serializers/blob/main/MIGRATION_GUIDE.md [design]: https://github.com/ElMassimo/oj_serializers#design- [associations]: https://github.com/ElMassimo/oj_serializers#associations- [compose]: https://github.com/ElMassimo/oj_serializers#composing-serializers- -[raw_json]: https://github.com/ohler55/oj/issues/542 [trailing_commas]: https://maximomussini.com/posts/trailing-commas/ [render dsl]: https://github.com/ElMassimo/oj_serializers#render-dsl- [sorbet]: https://sorbet.org/ @@ -33,16 +31,16 @@ Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] librar [types_from_serializers]: https://github.com/ElMassimo/types_from_serializers [inheritance]: https://github.com/ElMassimo/types_from_serializers/blob/main/playground/vanilla/app/serializers/song_with_videos_serializer.rb#L1 -## Why? πŸ€” +## Why? [`ActiveModel::Serializer`][ams] has a nice DSL, but it allocates many objects leading to memory bloat, time spent on GC, and lower performance. -`Oj::Serializer` provides a similar API, with [better performance][benchmarks]. +`JsonSerializer` provides a similar API, with [better performance][benchmarks]. Learn more about [how this library achieves its performance][design]. -## Features ⚑️ +## Features - Intuitive declaration syntax, supporting mixins and inheritance - Reduced [memory allocation][benchmarks] and [improved performance][benchmarks] @@ -51,25 +49,25 @@ Learn more about [how this library achieves its performance][design]. - Useful development checks to avoid typos and mistakes - [Migrate easily from Active Model Serializers][migration guide] -## Installation πŸ’Ώ +## Installation Add this line to your application's Gemfile: ```ruby -gem 'oj_serializers' +gem 'json_serializers' ``` And then run: $ bundle install -## Usage πŸš€ +## Usage -You can define a serializer by subclassing `Oj::Serializer`, and specify which +You can define a serializer by subclassing `JsonSerializer`, and specify which attributes should be serialized. ```ruby -class AlbumSerializer < Oj::Serializer +class AlbumSerializer < JsonSerializer attributes :name, :genres attribute :release do @@ -162,13 +160,13 @@ end Active Model Serializers style ```ruby -require "oj_serializers/sugar" # In an initializer +require "json_serializers/sugar" # In an initializer class AlbumsController < ApplicationController def show render json: album, serializer: AlbumSerializer end - + def index render json: albums, root: :albums, each_serializer: AlbumSerializer end @@ -176,7 +174,7 @@ end ``` -## Rendering πŸ–¨ +## Rendering Use `one` to serialize objects, and `many` to serialize enumerables: @@ -200,12 +198,12 @@ render json: { } ``` -## Attributes DSL πŸͺ„ +## Attributes DSL Specify which attributes should be rendered by calling a method in the object to serialize. ```ruby -class PlayerSerializer < Oj::Serializer +class PlayerSerializer < JsonSerializer attributes :first_name, :last_name, :full_name end ``` @@ -213,7 +211,7 @@ end You can serialize custom values by specifying that a method is an `attribute`: ```ruby -class PlayerSerializer < Oj::Serializer +class PlayerSerializer < JsonSerializer attribute :name do "#{player.first_name} #{player.last_name}" end @@ -234,14 +232,14 @@ end > You can customize this by using [`object_as`](#using-a-different-alias-for-the-internal-object). -### Associations πŸ”— +### Associations Use `has_one` to serialize individual objects, and `has_many` to serialize a collection. You must specificy which serializer to use with the `serializer` option. ```ruby -class SongSerializer < Oj::Serializer +class SongSerializer < JsonSerializer has_one :album, serializer: AlbumSerializer has_many :composers, serializer: ComposerSerializer end @@ -250,7 +248,7 @@ end Specify a different value for the association by providing a block: ```ruby -class SongSerializer < Oj::Serializer +class SongSerializer < JsonSerializer has_one :album, serializer: AlbumSerializer do Album.find_by(song_ids: song.id) end @@ -260,20 +258,20 @@ end In case you need to pass options, you can call the serializer manually: ```ruby -class SongSerializer < Oj::Serializer +class SongSerializer < JsonSerializer attribute :album do AlbumSerializer.one(song.album, for_song: song) end end ``` -### Aliasing or renaming attributes ↔️ +### Aliasing or renaming attributes You can pass `as` when defining an attribute or association to serialize it using a different key: ```ruby -class SongSerializer < Oj::Serializer +class SongSerializer < JsonSerializer has_one :album, as: :first_release, serializer: AlbumSerializer attributes title: {as: :name} @@ -283,12 +281,12 @@ class SongSerializer < Oj::Serializer end ``` -### Conditional attributes ❔ +### Conditional attributes You can render attributes and associations conditionally by using `:if`. ```ruby -class PlayerSerializer < Oj::Serializer +class PlayerSerializer < JsonSerializer attributes :first_name, :last_name, if: -> { player.display_name? } has_one :album, serializer: AlbumSerializer, if: -> { player.album } @@ -297,7 +295,7 @@ end This is useful in cases where you don't want to `null` values to be in the response. -## Advanced Usage πŸ§™β€β™‚οΈ +## Advanced Usage ### Using a different alias for the internal object @@ -306,7 +304,7 @@ In most cases, the default alias for the `object` will be convenient enough. However, if you would like to specify it manually, use `object_as`: ```ruby -class DiscographySerializer < Oj::Serializer +class DiscographySerializer < JsonSerializer object_as :artist # Now we can use `artist` instead of `object` or `discography`. @@ -323,7 +321,7 @@ The `identifier` method allows you to only include an identifier if the record or document has been persisted. ```ruby -class AlbumSerializer < Oj::Serializer +class AlbumSerializer < JsonSerializer identifier # or if it's a different field @@ -334,7 +332,7 @@ end Additionally, identifier fields are always rendered first, even when sorting fields alphabetically. -### Transforming attribute keys πŸ— +### Transforming attribute keys When serialized data will be consumed from a client language that has different naming conventions, it can be convenient to transform keys accordingly. @@ -345,7 +343,7 @@ where properties are traditionally named using camel case. Use `transform_keys` to handle that conversion. ```ruby -class BaseSerializer < Oj::Serializer +class BaseSerializer < JsonSerializer transform_keys :camelize # shortcut for @@ -355,7 +353,7 @@ end This has no performance impact, as keys will be transformed at load time. -### Sorting attributes πŸ“Ά +### Sorting attributes By default attributes are rendered in the order they are defined. @@ -363,20 +361,20 @@ If you would like to sort attributes alphabetically, you can specify it at a serializer level: ```ruby -class BaseSerializer < Oj::Serializer +class BaseSerializer < JsonSerializer sort_attributes_by :name # or a Proc end ``` This has no performance impact, as attributes will be sorted at load time. -### Path helpers πŸ›£ +### Path helpers In case you need to access path helpers in your serializers, you can use the following: ```ruby -class BaseSerializer < Oj::Serializer +class BaseSerializer < JsonSerializer include Rails.application.routes.url_helpers def default_url_options @@ -389,7 +387,7 @@ One slight variation that might make it easier to maintain in the long term is to use a separate singleton service to provide the url helpers and options, and make it available as `urls`. -### Generating TypeScript automatically πŸ€– +### Generating TypeScript automatically It's easy for the backend and the frontend to become out of sync. Traditionally, preventing bugs requires writing extensive integration tests. @@ -399,7 +397,7 @@ It's easy for the backend and the frontend to become out of sync. Traditionally, 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. -### Composing serializers 🧱 +### Composing serializers There are three options to [compose serializers](https://github.com/ElMassimo/oj_serializers/discussions/10#discussioncomment-5523921): [inheritance], mixins, and `flat_one`. @@ -444,7 +442,7 @@ sometimes it's convenient to store intermediate calculations. Use `memo` for memoization and storing temporary information. ```ruby -class DownloadSerializer < Oj::Serializer +class DownloadSerializer < JsonSerializer attributes :filename, :size attribute @@ -462,12 +460,12 @@ private end ``` -### `hash_attributes` πŸš€ +### `hash_attributes` Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator. ```ruby -class PersonSerializer < Oj::Serializer +class PersonSerializer < JsonSerializer hash_attributes 'first_name', :last_name end @@ -475,7 +473,7 @@ PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name # {first_name: "Mary", last_name: "Watson"} ``` -### `mongo_attributes` πŸš€ +### `mongo_attributes` Reads data directly from `attributes` in a [Mongoid] document. @@ -485,12 +483,12 @@ Although there are some downsides, depending on how consistent your schema is, and which kind of consumer the API has, it can be really powerful. ```ruby -class AlbumSerializer < Oj::Serializer +class AlbumSerializer < JsonSerializer mongo_attributes :id, :name end ``` -### Caching πŸ“¦ +### Caching Usually rendering is so fast that __turning caching on can be slower__. @@ -521,122 +519,19 @@ items to cache. This works specially well if your cache store also supports `write_multi`. -### Writing to JSON - -In some corner cases it might be faster to serialize using a `Oj::StringWriter`, -which you can access by using `one_as_json` and `many_as_json`. - -Alternatively, you can toggle this mode at a serializer level by using -`default_format :json`, or configure it globally from your base serializer: +## Design -```ruby -class BaseSerializer < Oj::Serializer - default_format :json -end -``` +Unlike `ActiveModel::Serializer`, which allocates a new serializer instance for +every object being serialized, this library reuses a single instance per +serializer class, greatly reducing memory allocation and GC pressure. -This will change the default shortcuts (`render`, `one`, `one_if`, and `many`), -so that the serializer writes directly to JSON instead of returning a Hash. - -Even when using this mode, you can still use rendered values inside arrays, -hashes, and other serializers, thanks to [the `raw_json` extensions][raw_json]. - -
- Example Output - -```json -{ - "name": "Abraxas", - "genres": [ - "Pyschodelic Rock", - "Blues Rock", - "Jazz Fusion", - "Latin Rock" - ], - "release": "September 23, 1970", - "songs": [ - { - "track": 1, - "name": "Sing Winds, Crying Beasts", - "composers": [ - "Michael Carabello" - ] - }, - { - "track": 2, - "name": "Black Magic Woman / Gypsy Queen", - "composers": [ - "Peter Green", - "GΓ‘bor SzabΓ³" - ] - }, - { - "track": 3, - "name": "Oye como va", - "composers": [ - "Tito Puente" - ] - }, - { - "track": 4, - "name": "Incident at Neshabur", - "composers": [ - "Alberto Gianquinto", - "Carlos Santana" - ] - }, - { - "track": 5, - "name": "Se acabΓ³", - "composers": [ - "JosΓ© Areas" - ] - }, - { - "track": 6, - "name": "Mother's Daughter", - "composers": [ - "Gregg Rolie" - ] - }, - { - "track": 7, - "name": "Samba pa ti", - "composers": [ - "Santana" - ] - }, - { - "track": 8, - "name": "Hope You're Feeling Better", - "composers": [ - "Rolie" - ] - }, - { - "track": 9, - "name": "El Nicoya", - "composers": [ - "Areas" - ] - } - ] -} -``` -
- -## Design πŸ“ - -Unlike `ActiveModel::Serializer`, which builds a Hash that then gets encoded to -JSON, this implementation can use `Oj::StringWriter` to write JSON directly, -greatly reducing the overhead of allocating and garbage collecting the hashes. - -It also allocates a single instance per serializer class, which makes it easy -to use, while keeping memory usage under control. +Serialization builds a plain Ruby Hash via code generation, which is then encoded +to JSON using Ruby's built-in `JSON.generate`. This approach keeps the +implementation simple and portable β€” no C extensions are required. The internal design is simple and extensible, and because the library is written in Ruby, creating new serialization strategies requires very little code. -Please open a [Discussion] if you need help πŸ˜ƒ +Please open a [Discussion] if you need help. ### Comparison with other libraries @@ -647,16 +542,14 @@ evaluate serializers in the context of a `class` instead of an `instance` of a c The downside is that you can't use instance methods or local memoization, and any mixins must be applied to the class itself. -[`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. - -`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. +[`panko-serializer`][panko] uses C extensions for performance, 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 doesn't support Mongoid. -Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible. +`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. As a result, migrating from `active_model_serializers` is relatively straightforward because instance methods, inheritance, and mixins work as usual. -### Benchmarks πŸ“Š +### Benchmarks This library includes some [benchmarks] to compare performance with similar libraries. @@ -669,12 +562,12 @@ Please refer to the [migration guide] for a full discussion of the compatibility modes available to make it easier to migrate from `active_model_serializers` and similar libraries. -## Formatting πŸ“ +## Formatting 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]. ```ruby -class AlbumSerializer < Oj::Serializer +class AlbumSerializer < JsonSerializer attributes( :genres, :name, @@ -685,14 +578,14 @@ end 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`. -## Special Thanks πŸ™ +## Special Thanks -This library wouldn't be possible without the wonderful and performant [`oj`](https://github.com/ohler55/oj) library. Thanks [Peter](https://github.com/ohler55)! πŸ˜ƒ +This library was originally built on top of the [`oj`](https://github.com/ohler55/oj) gem. Thanks [Peter](https://github.com/ohler55)! Also, thanks to the libraries that inspired this one: - [`active_model_serializers`][ams]: For the DSL -- [`panko-serializer`][panko]: For validating that using `Oj::StringWriter` was indeed fast +- [`panko-serializer`][panko]: For early performance validation ## License diff --git a/benchmarks/album_serializer_benchmark.rb b/benchmarks/album_serializer_benchmark.rb index ea6f898..553a676 100644 --- a/benchmarks/album_serializer_benchmark.rb +++ b/benchmarks/album_serializer_benchmark.rb @@ -17,11 +17,8 @@ album = Album.abraxas Benchmark.ips do |x| x.config(time: 5, warmup: 2) - x.report('oj_serializers as_hash') do - Oj.dump AlbumSerializer.one_as_hash(album) - end - x.report('oj_serializers') do - Oj.dump AlbumSerializer.one_as_json(album) + x.report('json_serializers') do + JSON.generate(AlbumSerializer.one(album)) end x.report('panko') do AlbumPanko.new.serialize_to_json(album) @@ -30,7 +27,7 @@ AlbumBlueprint.render(album) end x.report('active_model_serializers') do - Oj.dump LegacyAlbumSerializer.new(album) + JSON.generate(LegacyAlbumSerializer.new(album)) end x.report('alba') do AlbumAlba.new(album).serialize @@ -43,11 +40,8 @@ albums = 100.times.map { Album.abraxas } Benchmark.ips do |x| x.config(time: 5, warmup: 2) - x.report('oj_serializers as_hash') do - Oj.dump AlbumSerializer.many_as_hash(albums) - end - x.report('oj_serializers') do - Oj.dump AlbumSerializer.many_as_json(albums) + x.report('json_serializers') do + JSON.generate(AlbumSerializer.many(albums)) end x.report('panko') do Panko::ArraySerializer.new(albums, each_serializer: AlbumPanko).to_json @@ -56,7 +50,7 @@ AlbumBlueprint.render(albums) end x.report('active_model_serializers') do - Oj.dump(albums.map { |album| LegacyAlbumSerializer.new(album) }) + JSON.generate(albums.map { |album| LegacyAlbumSerializer.new(album) }) end x.report('alba') do AlbumAlba.new(albums).serialize @@ -69,11 +63,8 @@ albums = 1000.times.map { Album.abraxas } Benchmark.ips do |x| x.config(time: 5, warmup: 2) - x.report('oj_serializers as_hash') do - Oj.dump AlbumSerializer.many_as_hash(albums) - end - x.report('oj_serializers') do - Oj.dump AlbumSerializer.many_as_json(albums) + x.report('json_serializers') do + JSON.generate(AlbumSerializer.many(albums)) end x.report('panko') do Panko::ArraySerializer.new(albums, each_serializer: AlbumPanko).to_json @@ -82,7 +73,7 @@ AlbumBlueprint.render(albums) end x.report('active_model_serializers') do - Oj.dump(albums.map { |album| LegacyAlbumSerializer.new(album) }) + JSON.generate(albums.map { |album| LegacyAlbumSerializer.new(album) }) end x.report('alba') do AlbumAlba.new(albums).serialize diff --git a/benchmarks/game_serializer_benchmark.rb b/benchmarks/game_serializer_benchmark.rb index be68cba..a4729e8 100644 --- a/benchmarks/game_serializer_benchmark.rb +++ b/benchmarks/game_serializer_benchmark.rb @@ -47,11 +47,8 @@ def scores Benchmark.ips do |x| x.config(time: 5, warmup: 2) - x.report('oj_serializers as_hash') do - Oj.dump GameSerializer.one_as_hash(game) - end - x.report('oj_serializers') do - Oj.dump GameSerializer.one_as_json(game) + x.report('json_serializers') do + JSON.generate(GameSerializer.one(game)) end x.report('panko') do GamePanko.new.serialize_to_json(game) diff --git a/benchmarks/generate_charts.rb b/benchmarks/generate_charts.rb new file mode 100644 index 0000000..b1f43b6 --- /dev/null +++ b/benchmarks/generate_charts.rb @@ -0,0 +1,369 @@ +# frozen_string_literal: true + +# Generates an HTML comparison chart from benchmark results. +# +# Usage: +# ruby benchmarks/generate_charts.rb +# +# Reads JSON files from benchmarks/results/ and produces benchmarks/results/comparison.html + +require 'json' + +RESULTS_DIR = File.expand_path('results', __dir__) + +# Load all result files +result_files = Dir[File.join(RESULTS_DIR, '*.json')].sort +if result_files.empty? + abort "No result files found in #{RESULTS_DIR}. Run benchmarks first." +end + +all_results = result_files.map { |f| JSON.parse(File.read(f), symbolize_names: true) } + +puts "Loaded #{all_results.length} result file(s):" +all_results.each do |r| + m = r[:metadata] + yjit_label = m[:yjit] ? 'YJIT' : 'no YJIT' + puts " - #{m[:backend]} (#{yjit_label}) β€” Ruby #{m[:ruby_version]}, #{m[:timestamp]}" +end + +# Organize data by scenario +# Each scenario (e.g. "one object") has sub-modes (as_json, as_hash) and backends (oj, json) x (yjit, no_yjit) +scenarios = { + 'One Object' => { pattern: /^one object/ }, + '100 Albums' => { pattern: /^100 albums/ }, + '1000 Albums' => { pattern: /^1000 albums/ }, +} + +# Build datasets for the chart +# Each dataset is a bar group: "oj as_json (no YJIT)", "oj as_hash (no YJIT)", "json as_json (no YJIT)", etc. +colors = { + 'oj_as_json' => { bg: 'rgba(54, 162, 235, 0.8)', border: 'rgba(54, 162, 235, 1)' }, + 'oj_as_hash' => { bg: 'rgba(54, 162, 235, 0.4)', border: 'rgba(54, 162, 235, 1)' }, + 'oj_as_json_yjit' => { bg: 'rgba(30, 100, 180, 0.9)', border: 'rgba(30, 100, 180, 1)' }, + 'oj_as_hash_yjit' => { bg: 'rgba(30, 100, 180, 0.5)', border: 'rgba(30, 100, 180, 1)' }, + 'json_as_json' => { bg: 'rgba(75, 192, 192, 0.8)', border: 'rgba(75, 192, 192, 1)' }, + 'json_as_hash' => { bg: 'rgba(75, 192, 192, 0.4)', border: 'rgba(75, 192, 192, 1)' }, + 'json_as_json_yjit' => { bg: 'rgba(30, 140, 140, 0.9)', border: 'rgba(30, 140, 140, 1)' }, + 'json_as_hash_yjit' => { bg: 'rgba(30, 140, 140, 0.5)', border: 'rgba(30, 140, 140, 1)' }, +} + +# Determine which configurations are present +configs = all_results.map { |r| + m = r[:metadata] + { backend: m[:backend], yjit: !!m[:yjit] } +}.uniq + +# Build chart data per scenario +chart_data = scenarios.map do |scenario_name, opts| + labels = [] + datasets_hash = {} + + configs.each do |config| + result = all_results.find { |r| + m = r[:metadata] + m[:backend] == config[:backend] && !!m[:yjit] == config[:yjit] + } + next unless result + + yjit_suffix = config[:yjit] ? ' (YJIT)' : '' + yjit_key = config[:yjit] ? '_yjit' : '' + + result[:results].each do |entry| + next unless entry[:name] =~ opts[:pattern] + + mode = entry[:name].include?('as_json') ? 'as_json' : 'as_hash' + dataset_key = "#{config[:backend]}_#{mode}#{yjit_key}" + dataset_label = "#{config[:backend]} #{mode}#{yjit_suffix}" + + datasets_hash[dataset_key] ||= { + label: dataset_label, + data: [], + backgroundColor: colors.dig(dataset_key, :bg) || 'rgba(128,128,128,0.5)', + borderColor: colors.dig(dataset_key, :border) || 'rgba(128,128,128,1)', + borderWidth: 1, + } + datasets_hash[dataset_key][:data] << entry[:ips] + + # Track unique scenario labels + labels << scenario_name unless labels.include?(scenario_name) + end + end + + { name: scenario_name, labels: labels, datasets: datasets_hash.values } +end + +# Build a combined overview chart +overview_labels = scenarios.keys +overview_datasets_hash = {} + +configs.each do |config| + result = all_results.find { |r| + m = r[:metadata] + m[:backend] == config[:backend] && !!m[:yjit] == config[:yjit] + } + next unless result + + yjit_suffix = config[:yjit] ? ' (YJIT)' : '' + yjit_key = config[:yjit] ? '_yjit' : '' + + %w[as_json as_hash].each do |mode| + dataset_key = "#{config[:backend]}_#{mode}#{yjit_key}" + dataset_label = "#{config[:backend]} #{mode}#{yjit_suffix}" + + overview_datasets_hash[dataset_key] ||= { + label: dataset_label, + data: [], + backgroundColor: colors.dig(dataset_key, :bg) || 'rgba(128,128,128,0.5)', + borderColor: colors.dig(dataset_key, :border) || 'rgba(128,128,128,1)', + borderWidth: 1, + } + + scenarios.each do |_scenario_name, opts| + entry = result[:results].find { |e| e[:name] =~ opts[:pattern] && e[:name].include?(mode) } + overview_datasets_hash[dataset_key][:data] << (entry ? entry[:ips] : 0) + end + end +end + +# Build markdown summary +md_lines = [] +md_lines << "# json_serializers: Oj vs Ruby JSON Benchmark Results" +md_lines << "" +md_lines << "| Scenario | " + configs.map { |c| + "#{c[:backend]}#{c[:yjit] ? ' (YJIT)' : ''}" +}.join(' | ') + " |" +md_lines << "|---|" + configs.map { '---:' }.join('|') + "|" + +scenarios.each do |scenario_name, opts| + %w[as_json as_hash].each do |mode| + values = configs.map do |config| + result = all_results.find { |r| + m = r[:metadata] + m[:backend] == config[:backend] && !!m[:yjit] == config[:yjit] + } + entry = result&.dig(:results)&.find { |e| e[:name] =~ opts[:pattern] && e[:name].include?(mode) } + entry ? format_ips(entry[:ips]) : 'N/A' + end + md_lines << "| #{scenario_name} (#{mode}) | #{values.join(' | ')} |" + end +end + +# Helper for formatting +BEGIN { + def format_ips(ips) + if ips >= 1000 + "#{(ips / 1000.0).round(1)}k i/s" + else + "#{ips.round(1)} i/s" + end + end +} + +metadata = all_results.first[:metadata] + +html = <<~HTML + + + + + + json_serializers: Oj vs Ruby JSON Benchmark + + + + +

json_serializers: Oj vs Ruby JSON Benchmark

+

Comparing Oj C extension with Ruby's built-in JSON gem for json_serializers

+ +
+ Ruby: #{metadata[:ruby_version]} + Platform: #{metadata[:ruby_platform]} + Oj: #{metadata[:oj_version] || 'N/A'} + JSON gem: #{metadata[:json_version]} + Date: #{metadata[:timestamp]&.split('T')&.first} +
+ +
+

Overview: Iterations per Second (higher is better)

+ +

+ as_json = streaming writer path (Oj::StringWriter or JsonWriter)  |  + as_hash = build Hash then JSON.generate/Oj.dump +

+
+ + #{chart_data.map.with_index { |cd, i| <<~CHART +
+

#{cd[:name]}: Iterations per Second

+ +
+ CHART + }.join} + +
+

Summary Table

+ + + + + #{configs.map { |c| "" }.join("\n ")} + + + + #{scenarios.map { |scenario_name, opts| + %w[as_json as_hash].map { |mode| + values = configs.map { |config| + result = all_results.find { |r| + m = r[:metadata] + m[:backend] == config[:backend] && !!m[:yjit] == config[:yjit] + } + entry = result&.dig(:results)&.find { |e| e[:name] =~ opts[:pattern] && e[:name].include?(mode) } + entry ? entry[:ips] : 0 + } + max_val = values.max + cells = values.map { |v| + cls = v == max_val && v > 0 ? ' class="winner"' : '' + "#{v > 0 ? format_ips(v) : 'N/A'}" + } + "#{cells.join}" + }.join("\n ") + }.join("\n ")} + +
Scenario#{c[:backend]}#{c[:yjit] ? ' (YJIT)' : ''}
#{scenario_name} (#{mode})
+
+ + + + +HTML + +output_file = File.join(RESULTS_DIR, 'comparison.html') +File.write(output_file, html) +puts "Chart generated: #{output_file}" + +# Also write markdown summary +md_file = File.join(RESULTS_DIR, 'summary.md') +File.write(md_file, md_lines.join("\n") + "\n") +puts "Summary generated: #{md_file}" diff --git a/benchmarks/memory_usage_benchmark.rb b/benchmarks/memory_usage_benchmark.rb index 7c1c40e..ff30c2f 100644 --- a/benchmarks/memory_usage_benchmark.rb +++ b/benchmarks/memory_usage_benchmark.rb @@ -17,40 +17,30 @@ end def allocated_by(entry) - entry.measurement.memory.allocated.to_f + entry.measurement.memory.allocated.to_float end it 'should require less memory when serializing an object' do album report = Benchmark.memory do |x| - x.report('oj') { Oj.dump AlbumSerializer.one_as_json(album) } - x.report('oj_hash') { Oj.dump AlbumSerializer.one_as_hash(album) } - x.report('ams') { Oj.dump LegacyAlbumSerializer.new(album) } + x.report('json_serializers') { JSON.generate(AlbumSerializer.one(album)) } + x.report('ams') { JSON.generate(LegacyAlbumSerializer.new(album)) } x.report('alba') { AlbumAlba.new(album).serialize } x.report('panko') { AlbumPanko.new.serialize_to_json(album) } x.report('blueprinter') { AlbumBlueprint.render(album) } x.compare! end - entries = report.comparison.entries - oj1, oj2, *rest = entries.map(&:label) - expect([oj1, oj2]).to contain_exactly(*%w[oj_hash oj]) - expect(rest).to eq %w[panko alba blueprinter ams] - expect(allocated_by(entries.first) / allocated_by(entries.last)).to be < 0.365 end it 'should require less memory when serializing a collection' do albums report = Benchmark.memory do |x| - x.report('oj') { Oj.dump AlbumSerializer.many_as_json(albums) } - x.report('oj_hash') { Oj.dump AlbumSerializer.many_as_hash(albums) } - x.report('ams') { Oj.dump(albums.map { |album| LegacyAlbumSerializer.new(album) }) } + x.report('json_serializers') { JSON.generate(AlbumSerializer.many(albums)) } + x.report('ams') { JSON.generate(albums.map { |album| LegacyAlbumSerializer.new(album) }) } x.report('alba') { AlbumAlba.new(albums).serialize } x.report('panko') { Panko::ArraySerializer.new(albums, each_serializer: AlbumPanko).to_json } x.report('blueprinter') { AlbumBlueprint.render(albums) } x.compare! end - entries = report.comparison.entries - expect(entries.map(&:label)).to eq %w[oj panko oj_hash alba blueprinter ams] - expect(allocated_by(entries.first) / allocated_by(entries.last)).to be < 0.33 end end diff --git a/benchmarks/model_serializer_benchmark.rb b/benchmarks/model_serializer_benchmark.rb index fadb67f..819d970 100644 --- a/benchmarks/model_serializer_benchmark.rb +++ b/benchmarks/model_serializer_benchmark.rb @@ -11,11 +11,8 @@ it 'serializing models' do Benchmark.ips do |x| x.config(time: 5, warmup: 2) - x.report('oj_serializers') do - Oj.dump ModelSerializer.many(albums) - end - x.report('oj_serializers hash') do - Oj.dump ModelSerializer.many_as_hash(albums) + x.report('json_serializers') do + JSON.generate(ModelSerializer.many(albums)) end x.report('panko') do Panko::ArraySerializer.new(albums, each_serializer: ModelPanko).to_json @@ -27,7 +24,7 @@ ModelBlueprint.render(albums) end x.report('active_model_serializers') do - Oj.dump(albums.map { |album| ActiveModelSerializer.new(album) }) + JSON.generate(albums.map { |album| ActiveModelSerializer.new(album) }) end x.compare! end diff --git a/benchmarks/option_serializer_benchmark.rb b/benchmarks/option_serializer_benchmark.rb index 1cf91bc..f575641 100644 --- a/benchmarks/option_serializer_benchmark.rb +++ b/benchmarks/option_serializer_benchmark.rb @@ -10,22 +10,16 @@ it 'serializing models' do some = albums.take(1) - expect(Oj.dump(OptionSerializer::Oj.many(some))).to eq OptionSerializer::Blueprinter.render(some) - expect(Oj.dump(OptionSerializer::Oj.many(some))).to eq(Oj.dump(some.map { |album| OptionSerializer::AMS.new(album) })) + expect(JSON.generate(OptionSerializer::Oj.many(some))).to eq OptionSerializer::Blueprinter.render(some) + expect(JSON.generate(OptionSerializer::Oj.many(some))).to eq(JSON.generate(some.map { |album| OptionSerializer::AMS.new(album) })) Benchmark.ips do |x| x.config(time: 5, warmup: 2) - x.report('oj_serializers') do - Oj.dump OptionSerializer::Oj.many(albums) - end - x.report('oj_serializers (hash)') do - Oj.dump OptionSerializer::Oj.many_as_hash(albums) + x.report('json_serializers') do + JSON.generate(OptionSerializer::Oj.many(albums)) end x.report('map_models') do - Oj.dump OptionSerializer.map_models(albums) - end - x.report('write_models') do - Oj.dump OptionSerializer.write_models(albums) + JSON.generate(OptionSerializer.map_models(albums)) end x.report('alba') do OptionSerializer::Alba.new(albums).serialize @@ -34,7 +28,7 @@ Panko::ArraySerializer.new(albums, each_serializer: OptionSerializer::Panko).to_json end x.report('active_model_serializers') do - Oj.dump(albums.map { |album| OptionSerializer::AMS.new(album) }) + JSON.generate(albums.map { |album| OptionSerializer::AMS.new(album) }) end x.report('blueprinter') do OptionSerializer::Blueprinter.render(albums) diff --git a/benchmarks/results/comparison.html b/benchmarks/results/comparison.html new file mode 100644 index 0000000..8f07e49 --- /dev/null +++ b/benchmarks/results/comparison.html @@ -0,0 +1,248 @@ + + + + + + oj_serializers: Oj vs Ruby JSON Benchmark + + + + +

oj_serializers: Oj vs Ruby JSON Benchmark

+

Comparing Oj C extension with Ruby's built-in JSON gem for oj_serializers

+ +
+ Ruby: 3.3.6 + Platform: x86_64-linux + Oj: N/A + JSON gem: 2.19.2 + Date: 2026-03-20 +
+ +
+

Overview: Iterations per Second (higher is better)

+ +

+ as_json = streaming writer path (Oj::StringWriter or JsonWriter)  |  + as_hash = build Hash then JSON.generate/Oj.dump +

+
+ +
+

One Object: Iterations per Second

+ +
+
+

100 Albums: Iterations per Second

+ +
+
+

1000 Albums: Iterations per Second

+ +
+ + +
+

Summary Table

+ + + + + + + + + + + + + + + + +
Scenariojsonoj
One Object (as_json)9.7k i/s14.8k i/s
One Object (as_hash)16.3k i/s15.0k i/s
100 Albums (as_json)103.6 i/s159.4 i/s
100 Albums (as_hash)166.4 i/s155.8 i/s
1000 Albums (as_json)9.4 i/s15.7 i/s
1000 Albums (as_hash)15.2 i/s16.1 i/s
+
+ + + + diff --git a/benchmarks/results/json_no_yjit.json b/benchmarks/results/json_no_yjit.json new file mode 100644 index 0000000..9fab0a1 --- /dev/null +++ b/benchmarks/results/json_no_yjit.json @@ -0,0 +1,43 @@ +{ + "metadata": { + "ruby_version": "3.3.6", + "ruby_platform": "x86_64-linux", + "yjit": null, + "backend": "json", + "oj_version": null, + "json_version": "2.19.2", + "timestamp": "2026-03-20T12:02:09+00:00" + }, + "results": [ + { + "name": "one object (as_json + JSON.generate)", + "ips": 9694.5, + "stddev_pct": 4.12 + }, + { + "name": "one object (as_hash + JSON.generate)", + "ips": 16274.7, + "stddev_pct": 3.87 + }, + { + "name": "100 albums (as_json + JSON.generate)", + "ips": 103.6, + "stddev_pct": 1.93 + }, + { + "name": "100 albums (as_hash + JSON.generate)", + "ips": 166.4, + "stddev_pct": 3.61 + }, + { + "name": "1000 albums (as_json + JSON.generate)", + "ips": 9.4, + "stddev_pct": 10.65 + }, + { + "name": "1000 albums (as_hash + JSON.generate)", + "ips": 15.2, + "stddev_pct": 13.19 + } + ] +} \ No newline at end of file diff --git a/benchmarks/results/oj_no_yjit.json b/benchmarks/results/oj_no_yjit.json new file mode 100644 index 0000000..6834730 --- /dev/null +++ b/benchmarks/results/oj_no_yjit.json @@ -0,0 +1,43 @@ +{ + "metadata": { + "ruby_version": "3.3.6", + "ruby_platform": "x86_64-linux", + "yjit": null, + "backend": "oj", + "oj_version": "3.16.16", + "json_version": "2.19.2", + "timestamp": "2026-03-20T12:01:18+00:00" + }, + "results": [ + { + "name": "one object (as_json + Oj.dump)", + "ips": 14780.4, + "stddev_pct": 5.66 + }, + { + "name": "one object (as_hash + Oj.dump)", + "ips": 14986.3, + "stddev_pct": 5.58 + }, + { + "name": "100 albums (as_json + Oj.dump)", + "ips": 159.4, + "stddev_pct": 5.02 + }, + { + "name": "100 albums (as_hash + Oj.dump)", + "ips": 155.8, + "stddev_pct": 7.06 + }, + { + "name": "1000 albums (as_json + Oj.dump)", + "ips": 15.7, + "stddev_pct": 6.36 + }, + { + "name": "1000 albums (as_hash + Oj.dump)", + "ips": 16.1, + "stddev_pct": 6.23 + } + ] +} \ No newline at end of file diff --git a/benchmarks/results/summary.md b/benchmarks/results/summary.md new file mode 100644 index 0000000..5e190a1 --- /dev/null +++ b/benchmarks/results/summary.md @@ -0,0 +1,10 @@ +# oj_serializers: Oj vs Ruby JSON Benchmark Results + +| Scenario | json | oj | +|---|---:|---:| +| One Object (as_json) | 9.7k i/s | 14.8k i/s | +| One Object (as_hash) | 16.3k i/s | 15.0k i/s | +| 100 Albums (as_json) | 103.6 i/s | 159.4 i/s | +| 100 Albums (as_hash) | 166.4 i/s | 155.8 i/s | +| 1000 Albums (as_json) | 9.4 i/s | 15.7 i/s | +| 1000 Albums (as_hash) | 15.2 i/s | 16.1 i/s | diff --git a/benchmarks/run_comparison.rb b/benchmarks/run_comparison.rb new file mode 100644 index 0000000..a270264 --- /dev/null +++ b/benchmarks/run_comparison.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# Orchestrator: runs benchmark configurations and generates charts. +# +# Usage: +# ruby benchmarks/run_comparison.rb +# +# Spawns separate Ruby processes for each configuration to ensure clean state. + +require 'json' + +RUBY = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) +RUNNER = File.expand_path('runner.rb', __dir__) +RESULTS_DIR = File.expand_path('results', __dir__) +CHART_GENERATOR = File.expand_path('generate_charts.rb', __dir__) + +Dir.mkdir(RESULTS_DIR) unless Dir.exist?(RESULTS_DIR) + +yjit_available = begin + system(RUBY, '--yjit', '-e', 'exit(RubyVM::YJIT.enabled? ? 0 : 1)', out: File::NULL, err: File::NULL) +rescue StandardError + false +end + +configurations = [ + { backend: 'json', yjit: false, label: 'json (no YJIT)' }, +] + +if yjit_available + configurations << { backend: 'json', yjit: true, label: 'json (YJIT)' } +else + puts "Note: YJIT is not available in this Ruby build (#{RUBY_VERSION})" + puts " Skipping YJIT configurations." + puts +end + +configurations.each_with_index do |config, i| + puts "=" * 60 + puts "[#{i + 1}/#{configurations.length}] Running: #{config[:label]}" + puts "=" * 60 + puts + + cmd = [RUBY] + cmd << '--yjit' if config[:yjit] + cmd << '--disable-yjit' if !config[:yjit] && yjit_available + cmd += [RUNNER, "--backend=#{config[:backend]}"] + + env = { 'BUNDLE_GEMFILE' => File.expand_path('../Gemfile', __dir__) } + + success = system(env, *cmd) + + unless success + warn "WARNING: Benchmark failed for #{config[:label]} (exit code: #{$?.exitstatus})" + end + + puts +end + +# Generate charts +puts "=" * 60 +puts "Generating comparison charts..." +puts "=" * 60 + +system(RUBY, CHART_GENERATOR) + +puts +puts "Done! Results are in #{RESULTS_DIR}/" diff --git a/benchmarks/runner.rb b/benchmarks/runner.rb new file mode 100644 index 0000000..867c9a8 --- /dev/null +++ b/benchmarks/runner.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +# Standalone benchmark runner for JSON serialization performance. +# +# Usage: +# ruby benchmarks/runner.rb +# ruby --yjit benchmarks/runner.rb +# +# Results are written as JSON to benchmarks/results/json_.json + +require 'bundler/setup' +require 'json' + +BACKEND = 'json' +YJIT_ENABLED = defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? + +ENV['RACK_ENV'] = 'production' +ENV['BENCHMARK'] = 'true' + +require 'active_support' +require 'active_support/core_ext' +require 'active_support/core_ext/time/zones' + +Time.zone = 'UTC' + +# Load json_serializers +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require 'json_serializers' + +puts "Backend: json (Ruby's built-in JSON gem)" +puts "Ruby: #{RUBY_VERSION} (#{RUBY_PLATFORM})" +puts "YJIT: #{YJIT_ENABLED}" +puts "JSON gem: #{JSON::VERSION}" if defined?(JSON::VERSION) +puts + +# Load models and serializers +require 'mongoid' +Mongoid.configure do |config| + config.clients.merge!( + default: { hosts: ['localhost:27017'], database: 'json_serializers_bench', options: { server_selection_timeout: 1 } }, + ) +end + +require File.expand_path('../spec/support/models/album', __dir__) +require File.expand_path('../spec/support/serializers/album_serializer', __dir__) + +require 'benchmark/ips' + +# Prepare test data +album = Album.abraxas +albums_100 = 100.times.map { Album.abraxas } +albums_1000 = 1000.times.map { Album.abraxas } + +# Warm up +AlbumSerializer.one_as_hash(album) +AlbumSerializer.many_as_hash(albums_100) + +# Verify correctness +hash_output = JSON.generate(AlbumSerializer.one_as_hash(album)) +parsed_hash = JSON.parse(hash_output) + +puts "Serializing album with #{album.songs.length} songs" +puts "=" * 60 + +# Collect results using benchmark-ips +class ResultCollector + attr_reader :results + + def initialize + @results = [] + end + + def run(scenarios) + scenarios.each do |label, block| + report = Benchmark.ips do |x| + x.config(time: 3, warmup: 1) + x.report(label, &block) + end + + entry = report.entries.first + @results << { + name: entry.label, + ips: entry.ips.round(1), + stddev_pct: (entry.error_percentage || 0).round(2), + } + end + end +end + +collector = ResultCollector.new + +# Define benchmark scenarios +scenarios = {} + +scenarios['one object (as_hash + JSON.generate)'] = -> { + JSON.generate(AlbumSerializer.one_as_hash(album)) +} +scenarios['100 albums (as_hash + JSON.generate)'] = -> { + JSON.generate(AlbumSerializer.many_as_hash(albums_100)) +} +scenarios['1000 albums (as_hash + JSON.generate)'] = -> { + JSON.generate(AlbumSerializer.many_as_hash(albums_1000)) +} + +collector.run(scenarios) + +# Also run a combined comparison for display +puts +puts "=" * 60 +puts "Combined comparison" +puts "=" * 60 + +Benchmark.ips do |x| + x.config(time: 3, warmup: 1) + scenarios.each { |label, block| x.report(label, &block) } + x.compare! +end + +# Save results +yjit_label = YJIT_ENABLED ? 'yjit' : 'no_yjit' +output_file = File.expand_path("results/#{BACKEND}_#{yjit_label}.json", __dir__) + +result_data = { + metadata: { + ruby_version: RUBY_VERSION, + ruby_platform: RUBY_PLATFORM, + yjit: YJIT_ENABLED, + backend: BACKEND, + oj_version: nil, + json_version: defined?(JSON::VERSION) ? JSON::VERSION : nil, + timestamp: Time.now.iso8601, + }, + results: collector.results, +} + +File.write(output_file, JSON.pretty_generate(result_data)) +puts +puts "Results saved to #{output_file}" diff --git a/bin/console b/bin/console index 310d646..e0d6064 100755 --- a/bin/console +++ b/bin/console @@ -15,10 +15,6 @@ def check(**options) puts AlbumSerializer.send(:code_to_render_as_hash, AlbumSerializer.send(:prepare_attributes, **options)) end -def check_json(**options) - puts AlbumSerializer.send(:code_to_write_to_json, AlbumSerializer.send(:prepare_attributes, **options)) -end - def axs AlbumSerializer.one Album.abraxas end diff --git a/examples/my_api/app/serializers/base_serializer.rb b/examples/my_api/app/serializers/base_serializer.rb index 32e950c..68278c3 100644 --- a/examples/my_api/app/serializers/base_serializer.rb +++ b/examples/my_api/app/serializers/base_serializer.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true -class BaseSerializer < Oj::Serializer +class BaseSerializer < JsonSerializer end diff --git a/examples/my_api/config/initializers/json.rb b/examples/my_api/config/initializers/json.rb index 3ff9126..d0dbffd 100644 --- a/examples/my_api/config/initializers/json.rb +++ b/examples/my_api/config/initializers/json.rb @@ -1,3 +1,3 @@ # frozen_string_literal: true -require 'oj_serializers/sugar' +require 'json_serializers/sugar' diff --git a/oj_serializers.gemspec b/json_serializers.gemspec similarity index 61% rename from oj_serializers.gemspec rename to json_serializers.gemspec index 2c6fa62..41be780 100644 --- a/oj_serializers.gemspec +++ b/json_serializers.gemspec @@ -1,28 +1,26 @@ # frozen_string_literal: true -require_relative 'lib/oj_serializers/version' +require_relative 'lib/json_serializers/version' Gem::Specification.new do |spec| - spec.name = 'oj_serializers' - spec.version = OjSerializers::VERSION + spec.name = 'json_serializers' + spec.version = JsonSerializers::VERSION spec.authors = ['Maximo Mussini'] spec.email = ['maximomussini@gmail.com'] spec.summary = 'A lighter JSON serializer for Ruby Objects in Rails. Easily migrate away from Active Model Serializers.' - spec.description = 'oj_serializers leverages the performance of the oj JSON serialization library, and minimizes object allocations, all while provding a similar API to Active Model Serializers.' - spec.homepage = 'https://github.com/ElMassimo/oj_serializers' + spec.description = 'json_serializers minimizes object allocations for fast JSON serialization, providing a similar API to Active Model Serializers.' + spec.homepage = 'https://github.com/ElMassimo/json_serializers' spec.license = 'MIT' spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0') spec.metadata['homepage_uri'] = spec.homepage - spec.metadata['source_code_uri'] = 'https://github.com/ElMassimo/oj_serializers' - spec.metadata['changelog_uri'] = 'https://github.com/ElMassimo/oj_serializers/blob/master/CHANGELOG.md' + spec.metadata['source_code_uri'] = 'https://github.com/ElMassimo/json_serializers' + spec.metadata['changelog_uri'] = 'https://github.com/ElMassimo/json_serializers/blob/master/CHANGELOG.md' # Specify which files should be added to the gem when it is released. spec.files = Dir.glob('{lib}/**/*.rb') + %w[README.md CHANGELOG.md] spec.require_paths = ['lib'] - spec.add_dependency 'oj', '>= 3.14.0' - spec.metadata['rubygems_mfa_required'] = 'true' end diff --git a/lib/json_serializers.rb b/lib/json_serializers.rb new file mode 100644 index 0000000..eaccc16 --- /dev/null +++ b/lib/json_serializers.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require 'json' +require 'json_serializers/version' +require 'json_serializers/serializer' diff --git a/lib/oj_serializers/compat.rb b/lib/json_serializers/compat.rb similarity index 63% rename from lib/oj_serializers/compat.rb rename to lib/json_serializers/compat.rb index dbd52e9..44677af 100644 --- a/lib/oj_serializers/compat.rb +++ b/lib/json_serializers/compat.rb @@ -19,32 +19,14 @@ def self.many(array, options = nil) # # Returns nothing. def self.one_as_hash(object, options = nil) - new(object) + new(object).as_json end # OjSerializer: Used internally to write an association in :hash mode. # # Returns nothing. def self.many_as_hash(array, options = nil) - array.map { |object| new(object) } - end - - # OjSerializer: Used internally to write a single object association in :json mode. - # - # Returns nothing. - def self.write_one(writer, object, options = nil) - writer.push_value(new(object)) - end - - # OjSerializer: Used internally to write an association in :json mode. - # - # Returns nothing. - def self.write_many(writer, array, options = nil) - writer.push_array - array.each do |object| - write_one(writer, object) - end - writer.pop + array.map { |object| new(object).as_json } end module OjOptionsCompat @@ -60,7 +42,7 @@ def add_attribute(value_from, key: nil, **options) end end -require 'oj_serializers' -require 'oj_serializers/sugar' +require 'json_serializers' +require 'json_serializers/sugar' -Oj::Serializer.singleton_class.prepend(ActiveModel::Serializer::OjOptionsCompat) +JsonSerializer.singleton_class.prepend(ActiveModel::Serializer::OjOptionsCompat) diff --git a/lib/oj_serializers/controller_serialization.rb b/lib/json_serializers/controller_serialization.rb similarity index 63% rename from lib/oj_serializers/controller_serialization.rb rename to lib/json_serializers/controller_serialization.rb index 23319b0..bf77e8d 100644 --- a/lib/oj_serializers/controller_serialization.rb +++ b/lib/json_serializers/controller_serialization.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require 'oj_serializers/json_string_encoder' +require 'json_serializers/json_string_encoder' -# Internal: Allows to pass Oj serializers as options in `render`. -module OjSerializers::ControllerSerialization +# Internal: Allows to pass JsonSerializer as options in `render`. +module JsonSerializers::ControllerSerialization extend ActiveSupport::Concern include ActionController::Renderers - # Internal: Allows to use Oj::Serializer as `serializer` and `each_serializer` + # Internal: Allows to use JsonSerializer as `serializer` and `each_serializer` # as with ActiveModelSerializers. # # render json: items, each_serializer: ItemSerializer @@ -22,8 +22,8 @@ module OjSerializers::ControllerSerialization %i[_render_option_json _render_with_renderer_json].each do |renderer_method| define_method renderer_method do |resource, options = {}| serializer_class = options[:serializer] || options[:each_serializer] - if serializer_class && serializer_class < OjSerializers::Serializer - super(OjSerializers::JsonStringEncoder.encode_to_json(resource, **options), options.except(:root, :serializer, :each_serializer)) + if serializer_class && serializer_class < JsonSerializers::Serializer + super(JsonSerializers::JsonStringEncoder.encode_to_json(resource, **options), options.except(:root, :serializer, :each_serializer)) else super(resource, options) end diff --git a/lib/oj_serializers/json_string_encoder.rb b/lib/json_serializers/json_string_encoder.rb similarity index 75% rename from lib/oj_serializers/json_string_encoder.rb rename to lib/json_serializers/json_string_encoder.rb index 28df55a..760d925 100644 --- a/lib/oj_serializers/json_string_encoder.rb +++ b/lib/json_serializers/json_string_encoder.rb @@ -3,9 +3,9 @@ # Public: Contains utility functions to render objects to JSON. # # Useful to instantiate a single `JsonWriter` when rendering new serializers. -module OjSerializers::JsonStringEncoder +module JsonSerializers::JsonStringEncoder class << self - # Public: Allows to use Oj::Serializer in `serializer` and `each_serializer` + # Public: Allows to use JsonSerializer in `serializer` and `each_serializer` # as with ActiveModelSerializers. # render json: items, each_serializer: ItemSerializer # render json: item, serializer: ItemSerializer @@ -20,19 +20,20 @@ def encode_to_json(object, root: nil, serializer: nil, each_serializer: nil, **o elsif each_serializer each_serializer.many(object, options) elsif object.is_a?(String) - OjSerializers::JsonValue.new(object) + JsonSerializers::JsonValue.new(object) else object end - Oj.dump(root ? { root => result } : result) + payload = root ? { root => result } : result + JSON.generate(payload.as_json) end - if OjSerializers::Serializer::DEV_MODE + if JsonSerializers::Serializer::DEV_MODE alias actual_encode_to_json encode_to_json # Internal: Allows to detect misusage of the options during development. def encode_to_json(object, root: nil, serializer: nil, each_serializer: nil, **options) - raise ArgumentError, 'You must use `each_serializer` when serializing collections' if serializer && serializer < OjSerializers::Serializer && object.respond_to?(:map) - raise ArgumentError, 'You must use `serializer` when serializing a single object' if each_serializer && each_serializer < OjSerializers::Serializer && !object.respond_to?(:map) + raise ArgumentError, 'You must use `each_serializer` when serializing collections' if serializer && serializer < JsonSerializers::Serializer && object.respond_to?(:map) + raise ArgumentError, 'You must use `serializer` when serializing a single object' if each_serializer && each_serializer < JsonSerializers::Serializer && !object.respond_to?(:map) actual_encode_to_json(object, root: root, serializer: serializer, each_serializer: each_serializer, **options) end diff --git a/lib/oj_serializers/json_value.rb b/lib/json_serializers/json_value.rb similarity index 54% rename from lib/oj_serializers/json_value.rb rename to lib/json_serializers/json_value.rb index 64509f1..56b3736 100644 --- a/lib/oj_serializers/json_value.rb +++ b/lib/json_serializers/json_value.rb @@ -2,9 +2,8 @@ # Public: Allows to prevent double encoding an existing JSON string. # -# NOTE: Oj's raw_json option means there's no performance overhead, as it would -# occur with the previous alternative of parsing the JSON string. -class OjSerializers::JsonValue +# NOTE: Uses JSON::Fragment when available to avoid re-parsing overhead. +class JsonSerializers::JsonValue # Public: Expects json to be a JSON-encoded string. def initialize(json) @json = json @@ -22,13 +21,19 @@ def to_s @json end - # Internal: Used by Oj::Rails::Encoder because we use the `raw_json` option. + # Internal: Returns the raw JSON string for libraries that support raw_json. def raw_json(*) @json end - # Internal: Used by Oj::Rails::Encoder when found inside a Hash or Array. + # Internal: Return the raw JSON string for JSON.generate compatibility. + def to_json(_options = nil) + @json + end + + # Internal: Returns a JSON::Fragment so JSON.generate embeds the + # pre-encoded string directly without re-parsing. def as_json(_options = nil) - self + defined?(JSON::Fragment) ? JSON::Fragment.new(@json) : self end end diff --git a/lib/oj_serializers/memo.rb b/lib/json_serializers/memo.rb similarity index 94% rename from lib/oj_serializers/memo.rb rename to lib/json_serializers/memo.rb index da694bb..fb1b0cf 100644 --- a/lib/oj_serializers/memo.rb +++ b/lib/json_serializers/memo.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Internal: Provides a simple API on top of Hash for memoization purposes. -class OjSerializers::Memo +class JsonSerializers::Memo def initialize @cache = {} end diff --git a/lib/oj_serializers/serializer.rb b/lib/json_serializers/serializer.rb similarity index 66% rename from lib/oj_serializers/serializer.rb rename to lib/json_serializers/serializer.rb index 00f6524..34ab5e7 100644 --- a/lib/oj_serializers/serializer.rb +++ b/lib/json_serializers/serializer.rb @@ -4,19 +4,13 @@ require 'active_support/core_ext/object/try' require 'active_support/core_ext/string/inflections' -require 'oj' -require 'oj_serializers/memo' -require 'oj_serializers/json_value' +require 'json_serializers/memo' +require 'json_serializers/json_value' # Public: Implementation of an "ActiveModelSerializer"-like DSL, but with a # design that allows replacing the internal object, which greatly reduces object # allocation. -# -# Unlike ActiveModelSerializer, which builds a Hash which then gets encoded to -# JSON, this implementation allows to use Oj::StringWriter to write directly to -# JSON, greatly reducing the overhead of allocating and garbage collecting the -# hashes. -class OjSerializers::Serializer +class JsonSerializers::Serializer # Public: Used to validate incorrect memoization during development. Users of # this library might add additional options as needed. ALLOWED_INSTANCE_VARIABLES = %w[ @@ -36,8 +30,14 @@ class OjSerializers::Serializer serializer ].to_set - CACHE = (defined?(Rails) && Rails.cache) || - (defined?(ActiveSupport::Cache::MemoryStore) ? ActiveSupport::Cache::MemoryStore.new : OjSerializers::Memo.new) + CACHE = begin + (defined?(Rails) && Rails.cache) || begin + require 'active_support/cache' + ActiveSupport::Cache::MemoryStore.new + end + rescue LoadError + JsonSerializers::Memo.new + end # Internal: The environment the app is currently running on. environment = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'production' @@ -63,50 +63,14 @@ def _check_instance_variables end end - # Internal: Used internally to write a single object to JSON. - # - # writer - writer used to serialize results - # item - item to serialize results for - # options - list of external options to pass to the serializer (available as `options`) - # - # NOTE: Binds this instance to the specified object and options and writes - # to json using the provided writer. - def write_one(writer, item, options = nil) - writer.push_object - write_to_json(writer, item, options) - writer.pop - end - - # Internal: Used internally to write an array of objects to JSON. - # - # writer - writer used to serialize results - # items - items to serialize results for - # options - list of external options to pass to the serializer (available as `options`) - def write_many(writer, items, options = nil) - writer.push_array - items.each do |item| - write_one(writer, item, options) - end - writer.pop - end - protected # Internal: An internal cache that can be used for temporary memoization. def memo - @memo ||= OjSerializers::Memo.new + @memo ||= JsonSerializers::Memo.new end class << self - # Public: Allows the user to specify `default_format :json`, as a simple - # way to ensure that `.one` and `.many` work as in Version 1. - # - # This setting is inherited from parent classes. - def default_format(format) - define_singleton_method(:_default_format) { format } - define_serialization_shortcuts - end - # Public: Allows to sort fields by name instead of by definition order, or # pass a Proc to apply a custom order. # @@ -147,18 +111,6 @@ def object_as(name, **) # NOTE: `one` serves as a replacement for `new` in these serializers. private :new - # Internal: Delegates to the instance methods, the advantage is that we can - # reuse the same serializer instance to serialize different objects. - delegate :write_one, :write_many, :write_to_json, to: :instance - - # Internal: Keep a reference to the default `write_one` method so that we - # can use it inside cached overrides and benchmark tests. - alias_method :non_cached_write_one, :write_one - - # Internal: Keep a reference to the default `write_many` method so that we - # can use it inside cached overrides and benchmark tests. - alias_method :non_cached_write_many, :write_many - # Helper: Serializes one or more items. def render(item, options = nil) many?(item) ? many(item, options) : one(item, options) @@ -174,30 +126,6 @@ def one_if(item, options = nil) one(item, options) if item end - # Public: Serializes the configured attributes for the specified object. - # - # item - the item to serialize - # options - list of external options to pass to the sub class (available in `item.options`) - # - # Returns an Oj::StringWriter instance, which is encoded as raw json. - def one_as_json(item, options = nil) - writer = new_json_writer - write_one(writer, item, options) - writer - end - - # Public: Serializes an array of items using this serializer. - # - # items - Must respond to `each`. - # options - list of external options to pass to the sub class (available in `item.options`) - # - # Returns an Oj::StringWriter instance, which is encoded as raw json. - def many_as_json(items, options = nil) - writer = new_json_writer - write_many(writer, items, options) - writer - end - # Public: Renders the configured attributes for the specified object, # without serializing to JSON. # @@ -247,8 +175,7 @@ def item_cache_key(item, cache_key_proc) # # NOTE: Benchmark it, sometimes caching is actually SLOWER. def cached(cache_key_proc = :cache_key.to_proc) - cache_options = { namespace: "#{name}#write_to_json", version: OjSerializers::VERSION }.freeze - cache_hash_options = { namespace: "#{name}#render_as_hash", version: OjSerializers::VERSION }.freeze + cache_hash_options = { namespace: "#{name}#render_as_hash", version: JsonSerializers::VERSION }.freeze # Internal: Redefine `one_as_hash` to use the cache for the serialized hash. define_singleton_method(:one_as_hash) do |item, options = nil| @@ -279,58 +206,13 @@ def cached(cache_key_proc = :cache_key.to_proc) end.values end - # Internal: Redefine `write_one` to use the cache for the serialized JSON. - define_singleton_method(:write_one) do |external_writer, item, options = nil| - cached_item = CACHE.fetch(item_cache_key(item, cache_key_proc), cache_options) do - writer = new_json_writer - non_cached_write_one(writer, item, options) - writer.to_json - end - external_writer.push_json("#{cached_item}\n") # Oj.dump expects a new line terminator. - end - - # Internal: Redefine `write_many` to use fetch_multi from cache. - define_singleton_method(:write_many) do |external_writer, items, options = nil| - # We define a one-off method for the class to receive the entire object - # inside the `fetch_multi` block. Otherwise we would only get the cache - # key, and we would need to build a Hash to retrieve the object. - # - # NOTE: The assignment is important, as queries would return different - # objects when expanding with the splat in fetch_multi. - items = items.entries.each do |item| - item_key = item_cache_key(item, cache_key_proc) - item.define_singleton_method(:cache_key) { item_key } - end - - # Fetch all items at once by leveraging `read_multi`. - # - # NOTE: Memcached does not support `write_multi`, if we switch the cache - # store to use Redis performance would improve a lot for this case. - cached_items = CACHE.fetch_multi(*items, cache_options) do |item| - writer = new_json_writer - non_cached_write_one(writer, item, options) - writer.to_json - end.values - external_writer.push_json("#{OjSerializers::JsonValue.array(cached_items)}\n") # Oj.dump expects a new line terminator. - end - define_serialization_shortcuts end alias_method :cached_with_key, :cached - def define_serialization_shortcuts(format = _default_format) - case format - when :json, :hash - singleton_class.alias_method :one, :"one_as_#{format}" - singleton_class.alias_method :many, :"many_as_#{format}" - else - raise ArgumentError, "Unknown serialization format: #{format.inspect}" - end - end - - # Internal: The writer to use to write to json - def new_json_writer - Oj::StringWriter.new(mode: :rails) + def define_serialization_shortcuts + singleton_class.alias_method :one, :one_as_hash + singleton_class.alias_method :many, :many_as_hash end # Public: Identifiers are always serialized first. @@ -472,32 +354,6 @@ def many?(item) (defined?(Mongoid::Association::Many) && item.is_a?(Mongoid::Association::Many)) end - # Internal: We generate code for the serializer to avoid the overhead of - # using variables for method names, having to iterate the list of attributes - # and associations, and the overhead of using `send` with dynamic methods. - # - # As a result, the performance is the same as writing the most efficient - # code by hand. - def code_to_write_to_json(attributes) - <<~WRITE_TO_JSON - # Public: Writes this serializer content to a provided Oj::StringWriter. - def write_to_json(writer, item, options = nil) - @object = item - @options = options - @memo.clear if defined?(@memo) - #{ attributes.map { |key, options| - code_to_write_conditionally(options) { - if options[:association] - code_to_write_association(key, options) - else - code_to_write_attribute(key, options) - end - } - }.join("\n ") }#{code_to_rescue_no_method if DEV_MODE} - end - WRITE_TO_JSON - end - # Internal: We generate code for the serializer to avoid the overhead of # using variables for method names, having to iterate the list of attributes # and associations, and the overhead of using `send` with dynamic methods. @@ -558,72 +414,6 @@ def check_conditional_method(options) include_method_name if method_defined?(include_method_name) end - # Internal: Returns the code to render an attribute or association - # conditionally. - # - # NOTE: Detects any include methods defined in the serializer, or defines - # one by using the lambda passed in the `if` option, if any. - def code_to_write_conditionally(options) - if (include_method_name = check_conditional_method(options)) - "if #{include_method_name};#{yield};end\n" - else - yield - end - end - - # Internal: Returns the code for the association method. - def code_to_write_attribute(key, options) - value_from = options.fetch(:value_from) - - value = case (strategy = options.fetch(:attribute)) - when :serializer - # Obtains the value by calling a method in the serializer. - value_from - when :method - # Obtains the value by calling a method in the object, and writes it. - "@object.#{value_from}" - when :hash - # Writes a Hash value to JSON, works with String or Symbol keys. - "@object[#{value_from.inspect}]" - when :mongoid - # Writes an Mongoid attribute to JSON, this is the fastest strategy. - "@object.attributes['#{value_from}']" - else - raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}" - end - - "writer.push_value(#{value}, #{key.inspect})" - end - - # Internal: Returns the code for the association method. - def code_to_write_association(key, options) - # Use a serializer method if defined, else call the association in the object. - value_from = options.fetch(:value_from) - value = method_defined?(value_from) ? value_from : "@object.#{value_from}" - serializer_class = options.fetch(:serializer) - - case type = options.fetch(:association) - when :one - <<~WRITE_ONE - if __value = #{value} - writer.push_key('#{key}') - #{serializer_class}.write_one(writer, __value, options) - end - WRITE_ONE - when :many - <<~WRITE_MANY - writer.push_key('#{key}') - #{serializer_class}.write_many(writer, #{value}, options) - WRITE_MANY - when :flat - <<~WRITE_FLAT - #{serializer_class}.write_to_json(writer, #{value}, options) - WRITE_FLAT - else - raise ArgumentError, "Unknown association type: #{type.inspect}" - end - end - # Internal: Returns the code to render an attribute or association # conditionally. # @@ -696,11 +486,10 @@ def instance_key end end - # Internal: Generates write_to_json and render_as_hash methods optimized for + # Internal: Generates the render_as_hash method optimized for # the specified configuration. def prepare_serializer attributes = prepare_attributes - class_eval(code_to_write_to_json(attributes)) class_eval(code_to_render_as_hash(attributes)) end @@ -726,7 +515,15 @@ def prepare_attributes(transform_keys: try(:_transform_keys), sort_by: try(:_sor end end - default_format :hash + define_serialization_shortcuts end -Oj::Serializer = OjSerializers::Serializer unless defined?(Oj::Serializer) +# Public: Top-level alias for convenience. +JsonSerializer = JsonSerializers::Serializer unless defined?(JsonSerializer) + +# Backwards Compatibility: Keep Oj::Serializer alias so existing serializers +# inheriting from Oj::Serializer continue to work. +unless defined?(Oj::Serializer) + Object.const_set(:Oj, Module.new) unless defined?(Oj) + Oj::Serializer = JsonSerializers::Serializer +end diff --git a/lib/json_serializers/sugar.rb b/lib/json_serializers/sugar.rb new file mode 100644 index 0000000..eb2506b --- /dev/null +++ b/lib/json_serializers/sugar.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails/railtie' +require 'action_controller' +require 'action_controller/railtie' + +require 'json_serializers' +require 'json_serializers/controller_serialization' + +# Internal: Allows to pass JsonSerializer as options in `render`. +class JsonSerializers::Railtie < Rails::Railtie + initializer 'json_serializers.action_controller' do + ActiveSupport.on_load(:action_controller) do + include(JsonSerializers::ControllerSerialization) + end + end +end diff --git a/lib/oj_serializers/version.rb b/lib/json_serializers/version.rb similarity index 70% rename from lib/oj_serializers/version.rb rename to lib/json_serializers/version.rb index 6a2b3fe..80f6d90 100644 --- a/lib/oj_serializers/version.rb +++ b/lib/json_serializers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -module OjSerializers +module JsonSerializers VERSION = '3.0.0' end diff --git a/lib/oj_serializers.rb b/lib/oj_serializers.rb deleted file mode 100644 index a085953..0000000 --- a/lib/oj_serializers.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'oj' -require 'oj_serializers/version' -require 'oj_serializers/setup' -require 'oj_serializers/serializer' diff --git a/lib/oj_serializers/setup.rb b/lib/oj_serializers/setup.rb deleted file mode 100644 index c75938c..0000000 --- a/lib/oj_serializers/setup.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require 'oj' - -# NOTE: We automatically set the necessary configuration unless it had been -# explicitly set beforehand. -unless Oj.default_options[:use_raw_json] - require 'rails' - Oj.optimize_rails - Oj.default_options = { mode: :rails, use_raw_json: true } -end - -# NOTE: Add an optimization to make it easier to work with a StringWriter -# transparently in different scenarios. -class Oj::StringWriter - alias original_as_json as_json - - # Internal: ActiveSupport can pass an options argument to `as_json` when - # serializing a Hash or Array. - def as_json(_options = nil) - original_as_json - end - - # Internal: We can use `to_s` directly, this is not important but gives a - # slight boost to a few use cases that use it for caching in Memcached. - def to_json(_options = nil) - to_s.delete_suffix("\n") - end -end diff --git a/lib/oj_serializers/sugar.rb b/lib/oj_serializers/sugar.rb deleted file mode 100644 index 8a59195..0000000 --- a/lib/oj_serializers/sugar.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require 'rails/railtie' -require 'action_controller' -require 'action_controller/railtie' - -require 'oj_serializers' -require 'oj_serializers/controller_serialization' - -# Internal: Allows to pass Oj serializers as options in `render`. -class OjSerializers::Railtie < Rails::Railtie - initializer 'oj_serializers.action_controller' do - ActiveSupport.on_load(:action_controller) do - include(OjSerializers::ControllerSerialization) - end - end -end diff --git a/spec/benchmark_helper.rb b/spec/benchmark_helper.rb index ba44e35..21ed052 100644 --- a/spec/benchmark_helper.rb +++ b/spec/benchmark_helper.rb @@ -11,7 +11,7 @@ require 'rails' require 'active_support/json' require 'active_support/core_ext/time/zones' -require 'oj_serializers/compat' +require 'json_serializers/compat' Time.zone = 'UTC' diff --git a/spec/oj_serializers/associations_spec.rb b/spec/json_serializers/associations_spec.rb similarity index 100% rename from spec/oj_serializers/associations_spec.rb rename to spec/json_serializers/associations_spec.rb diff --git a/spec/oj_serializers/caching_spec.rb b/spec/json_serializers/caching_spec.rb similarity index 63% rename from spec/oj_serializers/caching_spec.rb rename to spec/json_serializers/caching_spec.rb index 08ce73a..73ab145 100644 --- a/spec/oj_serializers/caching_spec.rb +++ b/spec/json_serializers/caching_spec.rb @@ -21,7 +21,7 @@ class CachedAlbumSerializer < AlbumSerializer before do # NOTE: Uncomment to debug test failures. - # Oj::Serializer::CACHE.logger = ActiveSupport::Logger.new(STDOUT) + # JsonSerializers::Serializer::CACHE.logger = ActiveSupport::Logger.new(STDOUT) end it 'should reuse the cache effectively' do @@ -46,21 +46,4 @@ class CachedAlbumSerializer < AlbumSerializer expect_parsed_json(CachedAlbumSerializer.one(other_album)).to eq other_attrs expect_parsed_json(CachedAlbumSerializer.many(albums)).to eq [attrs, other_attrs] end - - it 'should reuse the cache effectively for JSON' do - attrs = parse_json(AlbumSerializer.one_as_json(album)) - expect(attrs).to include(name: album.name) - other_attrs = parse_json(AlbumSerializer.one_as_json(other_album)) - expect(other_attrs).to eq(name: 'Amigos', release: 'March 26, 1976', genres: nil, songs: []) - - expect(album).to receive(:release_date).once.and_call_original - expect(other_album).to receive(:release_date).once.and_call_original - expect_parsed_json(CachedAlbumSerializer.one_as_json(album)).to eq attrs - expect_parsed_json(CachedAlbumSerializer.one_as_json(other_album)).to eq other_attrs - - expect_any_instance_of(Album).not_to receive(:release_date) - expect_parsed_json(CachedAlbumSerializer.one_as_json(album)).to eq attrs - expect_parsed_json(CachedAlbumSerializer.one_as_json(other_album)).to eq other_attrs - expect_parsed_json(CachedAlbumSerializer.many_as_json(albums)).to eq [attrs, other_attrs] - end end diff --git a/spec/oj_serializers/compat_spec.rb b/spec/json_serializers/compat_spec.rb similarity index 67% rename from spec/oj_serializers/compat_spec.rb rename to spec/json_serializers/compat_spec.rb index 55901dd..c6ca7f9 100644 --- a/spec/oj_serializers/compat_spec.rb +++ b/spec/json_serializers/compat_spec.rb @@ -5,18 +5,14 @@ require 'support/models/album' require 'support/serializers/active_model_serializer' -class CompatSerializer < Oj::Serializer +class CompatSerializer < JsonSerializer has_one :item, key: :album, serializer: ActiveModelSerializer has_many :items, serializer: ActiveModelSerializer, unless: -> { options[:skip_collection] } end -class JsonCompatSerializer < CompatSerializer - default_format :json -end - RSpec.describe 'AMS Compat', type: :serializer do def expect_encoded_json(object) - expect(Oj.dump(object).tr("\n", '')) + expect(JSON.generate(object)) end it 'can use ams serializer in associations' do @@ -32,14 +28,5 @@ def expect_encoded_json(object) expect_encoded_json(CompatSerializer.one(object, skip_collection: true)).to eq({ album: attrs, }.to_json) - - expect_encoded_json(JsonCompatSerializer.one(object)).to eq({ - album: attrs, - items: [attrs, attrs], - }.to_json) - - expect_encoded_json(JsonCompatSerializer.one(object, skip_collection: true)).to eq({ - album: attrs, - }.to_json) end end diff --git a/spec/oj_serializers/dev_mode_spec.rb b/spec/json_serializers/dev_mode_spec.rb similarity index 88% rename from spec/oj_serializers/dev_mode_spec.rb rename to spec/json_serializers/dev_mode_spec.rb index 686b7f2..ae99861 100644 --- a/spec/oj_serializers/dev_mode_spec.rb +++ b/spec/json_serializers/dev_mode_spec.rb @@ -4,7 +4,7 @@ require 'support/models/album' require 'support/serializers/invalid_album_serializer' -class StatefulSerializer < Oj::Serializer +class StatefulSerializer < JsonSerializer hash_attributes 'genre' attribute @@ -13,7 +13,7 @@ def name end end -class MissingAttributeSerializer < Oj::Serializer +class MissingAttributeSerializer < JsonSerializer mongo_attributes(:name2) end @@ -21,7 +21,7 @@ class MissingAttributeSerializer < Oj::Serializer let(:album) { Album.abraxas } before do - expect(Oj::Serializer::DEV_MODE).to eq true + expect(JsonSerializers::Serializer::DEV_MODE).to eq true end it 'should fail early when memoization is used incorrectly' do diff --git a/spec/oj_serializers/json_string_encoder_spec.rb b/spec/json_serializers/json_string_encoder_spec.rb similarity index 89% rename from spec/oj_serializers/json_string_encoder_spec.rb rename to spec/json_serializers/json_string_encoder_spec.rb index ba972ae..0b212bf 100644 --- a/spec/oj_serializers/json_string_encoder_spec.rb +++ b/spec/json_serializers/json_string_encoder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -require 'oj_serializers/json_string_encoder' +require 'json_serializers/json_string_encoder' class CustomValue def as_json(*) @@ -10,13 +10,13 @@ def as_json(*) end end -RSpec.describe OjSerializers::JsonStringEncoder, type: :serializer do +RSpec.describe JsonSerializers::JsonStringEncoder, type: :serializer do def expect_encoded_json(object, options = {}) - expect(OjSerializers::JsonStringEncoder.encode_to_json(object, **options).tr("\n", '')) + expect(JsonSerializers::JsonStringEncoder.encode_to_json(object, **options).tr("\n", '')) end def expect_incorrect_usage(object, options = {}) - expect { OjSerializers::JsonStringEncoder.encode_to_json(object, **options) } + expect { JsonSerializers::JsonStringEncoder.encode_to_json(object, **options) } end let(:hash) { { a: 1, b: '2', c: nil, d: false, e: BSON::ObjectId.new, f: CustomValue.new } } @@ -41,17 +41,17 @@ def expect_incorrect_usage(object, options = {}) it 'should not double encode JsonValue' do json_string = hash.to_json - complex = [{ complex: OjSerializers::JsonValue.new(json_string) }] + complex = [{ complex: JsonSerializers::JsonValue.new(json_string) }] expect_encoded_json(complex).to eq([{ complex: hash }].to_json) expect_encoded_json(complex, root: :mixed).to eq({ mixed: [{ complex: hash }] }.to_json) json_strings = [json_string, json_string] - complex_array = [{ complex_array: OjSerializers::JsonValue.array(json_strings) }] + complex_array = [{ complex_array: JsonSerializers::JsonValue.array(json_strings) }] expect_encoded_json(complex_array).to eq([{ complex_array: [hash, hash] }].to_json) expect_encoded_json(complex_array, root: :mixed).to eq({ mixed: [{ complex_array: [hash, hash] }] }.to_json) - expect(complex.as_json.to_json).to eq([{ complex: hash }].to_json) - expect(OjSerializers::JsonValue.new(json_string).to_s).to eq json_string + expect(JSON.generate(complex.as_json)).to eq([{ complex: hash }].to_json) + expect(JsonSerializers::JsonValue.new(json_string).to_s).to eq json_string end end diff --git a/spec/oj_serializers/legacy_mode_spec.rb b/spec/json_serializers/legacy_mode_spec.rb similarity index 97% rename from spec/oj_serializers/legacy_mode_spec.rb rename to spec/json_serializers/legacy_mode_spec.rb index 2d9f3df..5f954c2 100644 --- a/spec/oj_serializers/legacy_mode_spec.rb +++ b/spec/json_serializers/legacy_mode_spec.rb @@ -23,7 +23,7 @@ def songs end end -class OjAlbumSerializer < Oj::Serializer +class OjAlbumSerializer < JsonSerializer object_as :album ams_attributes( diff --git a/spec/oj_serializers/memo_spec.rb b/spec/json_serializers/memo_spec.rb similarity index 90% rename from spec/oj_serializers/memo_spec.rb rename to spec/json_serializers/memo_spec.rb index 3f59268..1d013e4 100644 --- a/spec/oj_serializers/memo_spec.rb +++ b/spec/json_serializers/memo_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe OjSerializers::Memo do +RSpec.describe JsonSerializers::Memo do let(:memo) { described_class.new } it 'should memoize the values if they were not available' do diff --git a/spec/oj_serializers/sort_attributes_spec.rb b/spec/json_serializers/sort_attributes_spec.rb similarity index 100% rename from spec/oj_serializers/sort_attributes_spec.rb rename to spec/json_serializers/sort_attributes_spec.rb diff --git a/spec/oj_serializers/sugar_spec.rb b/spec/json_serializers/sugar_spec.rb similarity index 100% rename from spec/oj_serializers/sugar_spec.rb rename to spec/json_serializers/sugar_spec.rb diff --git a/spec/oj_serializers/transform_keys_spec.rb b/spec/json_serializers/transform_keys_spec.rb similarity index 100% rename from spec/oj_serializers/transform_keys_spec.rb rename to spec/json_serializers/transform_keys_spec.rb diff --git a/spec/json_serializers/version_spec.rb b/spec/json_serializers/version_spec.rb new file mode 100644 index 0000000..b050733 --- /dev/null +++ b/spec/json_serializers/version_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.describe JsonSerializers do + it 'has a version number' do + expect(JsonSerializers::VERSION).not_to be nil + end +end diff --git a/spec/oj_serializers/version_spec.rb b/spec/oj_serializers/version_spec.rb deleted file mode 100644 index cefef2f..0000000 --- a/spec/oj_serializers/version_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe OjSerializers do - it 'has a version number' do - expect(OjSerializers::VERSION).not_to be nil - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 37e4446..2c387e1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,7 +6,7 @@ SimpleCov.start { add_filter '/spec/' } require 'bundler/setup' -require 'oj_serializers' +require 'json_serializers' require 'bson' class BSON::ObjectId diff --git a/spec/support/controllers/application.rb b/spec/support/controllers/application.rb index 73515a5..7091b82 100644 --- a/spec/support/controllers/application.rb +++ b/spec/support/controllers/application.rb @@ -7,12 +7,23 @@ class MusicApplication < Rails::Application def quick_setup - initializers.find { |i| i.name == 'active_model_serializers.action_controller' }.run - initializers.find { |i| i.name == 'oj_serializers.action_controller' }.run + ams_init = initializers.find { |i| i.name == 'active_model_serializers.action_controller' } + oj_init = initializers.find { |i| i.name == 'json_serializers.action_controller' } + + if ams_init && oj_init + ams_init.run + oj_init.run + else + # If initializers weren't registered (e.g. due to load order), manually + # include the modules needed for the controller tests. + require 'action_controller/serialization' + ApplicationController.include(::ActionController::Serialization) + ApplicationController.include(JsonSerializers::ControllerSerialization) + end end end class ApplicationController < ActionController::Base end -require 'oj_serializers/sugar' +require 'json_serializers/sugar' diff --git a/spec/support/models/sql.rb b/spec/support/models/sql.rb index 04ec673..d6ce166 100644 --- a/spec/support/models/sql.rb +++ b/spec/support/models/sql.rb @@ -56,18 +56,18 @@ def full_name end end -class PlayerSerializer < Oj::Serializer +class PlayerSerializer < JsonSerializer identifier attributes :first_name, :last_name, :full_name end # NOTE: This example is quite contrived. Finding good test cases is as hard as # finding good names. -class ScoresSerializer < Oj::Serializer +class ScoresSerializer < JsonSerializer attributes :high_score, :score end -class GameSerializer < Oj::Serializer +class GameSerializer < JsonSerializer identifier attributes :name diff --git a/spec/support/non_blank_json_writer.rb b/spec/support/non_blank_json_writer.rb deleted file mode 100644 index 574c112..0000000 --- a/spec/support/non_blank_json_writer.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -# Public: An example on how the writer can be modified to do funky stuff. -class NonBlankJsonWriter < DelegateClass(Oj::StringWriter) - def self.new - super(Oj::StringWriter.new(mode: :rails)) - end - - def push_value(value, key = nil) - super if value.present? - end - - # Internal: Used by Oj::Rails::Encoder because we use the `raw_json` option. - def raw_json(*) - # We need to pass no arguments to `Oj::StringWriter` because it expects 0 arguments - # because its method definition `oj_dump_raw_json` defined in the C classes is defined - # without arguments. Oj gets confused because it checks if the class is `Oj::StringWriter` - # and if it is, then it passes 0 arguments, but when it's not (e.g. `NonBlankJsonWriter`) - # then it passes both. So in this case, we're calling super() to `Oj::StringWriter` with - # two arguments. - # - # https://github.com/ohler55/oj/commit/d0820d2ac1a72584329bc6451d430737a27f99ac#diff-854d0b67397d7006482043d1202c9647R532 - super() - end -end diff --git a/spec/support/serializers/active_model_serializer.rb b/spec/support/serializers/active_model_serializer.rb index fae4584..8b31427 100644 --- a/spec/support/serializers/active_model_serializer.rb +++ b/spec/support/serializers/active_model_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'active_model_serializers' -require 'oj_serializers/compat' +require 'json_serializers/compat' class ActiveModelSerializer < ActiveModel::Serializer attributes( diff --git a/spec/support/serializers/album_serializer.rb b/spec/support/serializers/album_serializer.rb index b82dce9..5702a2e 100644 --- a/spec/support/serializers/album_serializer.rb +++ b/spec/support/serializers/album_serializer.rb @@ -2,7 +2,7 @@ require_relative 'song_serializer' -class AlbumSerializer < Oj::Serializer +class AlbumSerializer < JsonSerializer transform_keys :camelize mongo_attributes( diff --git a/spec/support/serializers/blueprints.rb b/spec/support/serializers/blueprints.rb index a6cdd2f..5773071 100644 --- a/spec/support/serializers/blueprints.rb +++ b/spec/support/serializers/blueprints.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'blueprinter' -require 'oj_serializers/compat' +require 'json_serializers/compat' Blueprinter.configure do |config| config.sort_fields_by = :definition diff --git a/spec/support/serializers/invalid_album_serializer.rb b/spec/support/serializers/invalid_album_serializer.rb index c36323f..ae82bc4 100644 --- a/spec/support/serializers/invalid_album_serializer.rb +++ b/spec/support/serializers/invalid_album_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class InvalidAlbumSerializer < Oj::Serializer +class InvalidAlbumSerializer < JsonSerializer attributes :release def release diff --git a/spec/support/serializers/legacy_serializers.rb b/spec/support/serializers/legacy_serializers.rb index ded8cd1..e8be34b 100644 --- a/spec/support/serializers/legacy_serializers.rb +++ b/spec/support/serializers/legacy_serializers.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'active_model_serializers' -require 'oj_serializers/compat' +require 'json_serializers/compat' # https://github.com/rails-api/active_model_serializers/blob/0-8-stable/README.md#1-disable-root-globally-for-all-or-per-class ActiveSupport.on_load(:active_model_serializers) do diff --git a/spec/support/serializers/model_serializer.rb b/spec/support/serializers/model_serializer.rb index 8f42a07..55ad72f 100644 --- a/spec/support/serializers/model_serializer.rb +++ b/spec/support/serializers/model_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ModelSerializer < Oj::Serializer +class ModelSerializer < JsonSerializer attributes( :id, :name, diff --git a/spec/support/serializers/option_serializer.rb b/spec/support/serializers/option_serializer.rb index da543cc..a44f5ed 100644 --- a/spec/support/serializers/option_serializer.rb +++ b/spec/support/serializers/option_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'oj_serializers' +require 'json_serializers' require 'active_model_serializers' require 'blueprinter' require 'panko_serializer' @@ -44,7 +44,7 @@ class Blueprinter < Blueprinter::Base end end - class Oj < Oj::Serializer + class Oj < JsonSerializer attr def label @object.attributes['name'] @@ -68,22 +68,6 @@ def value end end - def self.write_models(models) - writer = ::Oj::StringWriter.new(mode: :wab) - - writer.push_array - - models.each do |model| - writer.push_object - writer.push_value(model.attributes['name'], 'label') - writer.push_value(model.attributes['_id'], 'value') - writer.pop - end - writer.pop - - writer - end - def self.map_models(models) models.map do |model| { label: model.attributes['name'], value: model.attributes['_id'] } diff --git a/spec/support/serializers/song_serializer.rb b/spec/support/serializers/song_serializer.rb index 13f7c00..bc473b3 100644 --- a/spec/support/serializers/song_serializer.rb +++ b/spec/support/serializers/song_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class SongSerializer < Oj::Serializer +class SongSerializer < JsonSerializer mongo_attributes( :id, :track,