Skip to content

Make oj optional: support pure-Ruby JSON fallback#39

Open
ElMassimo wants to merge 6 commits intomainfrom
claude/oj-vs-json-benchmark-hIQIZ
Open

Make oj optional: support pure-Ruby JSON fallback#39
ElMassimo wants to merge 6 commits intomainfrom
claude/oj-vs-json-benchmark-hIQIZ

Conversation

@ElMassimo
Copy link
Copy Markdown
Owner

Summary

This change makes the oj gem optional for oj_serializers by implementing a pure-Ruby fallback when oj is not available. The library now gracefully degrades to using Ruby's built-in JSON gem instead of requiring oj as a hard dependency.

Key Changes

  • Made oj optional in gemspec: Changed oj from a required dependency to an optional one, allowing the library to work without it installed.

  • Implemented JsonWriter fallback: Added OjSerializers::JsonWriter, a pure-Ruby replacement for Oj::StringWriter that builds JSON via string concatenation. It implements the same API used by generated serialization code, enabling seamless fallback when oj is unavailable.

  • Updated library initialization: Modified lib/oj_serializers.rb to conditionally require oj and its setup, falling back to loading JsonWriter when oj is not available.

  • Enhanced Serializer#new_json_writer: Updated to detect whether Oj::StringWriter is available and use either the oj implementation or the pure-Ruby JsonWriter accordingly.

  • Fixed JsonValue compatibility: Updated JsonValue#as_json to return a JSON::Fragment when oj is not loaded, ensuring pre-encoded JSON strings are properly embedded by JSON.generate.

  • Fixed json_string_encoder: Corrected the fallback to use JSON.generate instead of Oj.dump when oj is unavailable.

Benchmark Infrastructure

Added comprehensive benchmarking tools to measure performance differences:

  • benchmarks/runner.rb: Standalone benchmark runner that tests both backends (oj and json) with optional YJIT support, measuring iterations per second across three scenarios (one object, 100 albums, 1000 albums).

  • benchmarks/run_comparison.rb: Orchestrator that spawns separate Ruby processes for each configuration to ensure clean state, then generates comparison charts.

  • benchmarks/generate_charts.rb: Generates interactive HTML charts and markdown summaries from benchmark results using Chart.js.

  • gemfiles/Gemfile-json-only: Dedicated Gemfile for json-only benchmarking that excludes oj to ensure clean backend isolation.

  • Sample benchmark results: Included baseline results showing oj's performance advantage in streaming scenarios (as_json) while as_hash approaches parity.

Notable Implementation Details

  • The JsonWriter maintains an internal stack to track nesting context (objects/arrays) and properly handles comma insertion between elements.
  • Backend isolation is enforced during benchmarking by blocking oj imports when testing the json backend.
  • The fallback is transparent to users—serializers work identically regardless of which backend is available.
  • Benchmark results are saved as JSON for programmatic analysis and chart generation.

https://claude.ai/code/session_018LrqGa8M4Cnvt8cZ2489Vn

claude added 6 commits March 20, 2026 12:03
- Create pure-Ruby JsonWriter that implements Oj::StringWriter API via
  string concatenation, enabling the library to work without the oj gem
- Make oj a soft dependency: loads oj if available, falls back to json gem
- Add JSON::Fragment support in JsonValue for pre-encoded JSON embedding
- Create benchmark harness comparing oj vs Ruby's built-in JSON gem:
  - benchmarks/runner.rb: standalone runner with --backend=oj|json flag
  - benchmarks/run_comparison.rb: orchestrator spawning isolated processes
  - benchmarks/generate_charts.rb: HTML chart generator with Chart.js
- Store benchmark results and comparison charts in benchmarks/results/
- Update oj to 3.16.16, json gem to 2.19.2

Key findings (Ruby 3.3.6, no YJIT):
- Hash path + JSON.generate matches or beats Oj for all scenarios
- Pure-Ruby JsonWriter is ~1.7x slower than Oj::StringWriter
- The json gem has closed the gap significantly with oj

https://claude.ai/code/session_018LrqGa8M4Cnvt8cZ2489Vn
Remove the hard dependency on oj from the gemspec. The library now works
with either oj (for maximum performance via Oj::StringWriter) or Ruby's
built-in JSON gem (via the pure-Ruby JsonWriter fallback).

Users who want the oj backend can add `gem 'oj'` to their Gemfile.

https://claude.ai/code/session_018LrqGa8M4Cnvt8cZ2489Vn
Remove OjSerializers::JsonWriter, Oj::StringWriter integration, and all
write_ APIs (write_one, write_many, write_to_json). Remove default_format
since the format is always :hash. Simplify define_serialization_shortcuts,
cached method, and code generation to only use the hash path.

- Delete lib/oj_serializers/json_writer.rb and setup.rb
- Remove code_to_write_to_json, code_to_write_attribute, code_to_write_association
- Remove one_as_json, many_as_json, new_json_writer
- Simplify json_string_encoder to use JSON.generate
- Remove defined?(Oj) checks from json_value.rb
- Remove write_one/write_many from compat.rb
- Update all benchmarks to use JSON.generate instead of Oj.dump
- Delete gemfiles/Gemfile-json-only and spec/support/non_blank_json_writer.rb

https://claude.ai/code/session_018LrqGa8M4Cnvt8cZ2489Vn
Rename directories, files, modules, and class references throughout:
- lib/oj_serializers/ → lib/json_serializers/
- OjSerializers → JsonSerializers
- Oj::Serializer → JsonSerializer (with backward-compat alias)
- Update README to emphasize hash-based architecture and memory efficiency
- Update MIGRATION_GUIDE.md and CHANGELOG.md references

https://claude.ai/code/session_018LrqGa8M4Cnvt8cZ2489Vn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants