v2.0.0
2.0.0 (2026-09-01)
Graphiti 2.0
Graphiti 2.0 collapses the satellite-gem constellation into one gem, makes persistence behave the way you always assumed it did, and ships with a rebuilt documentation site. There are some important breaking changes that improve ergonomics, but nearly everything from 1.x still runs under 2.0 with deprecation warnings, so most apps can upgrade incrementally. Upgrade path here: https://graphiti.dev/upgrading/
One gem instead of four
graphiti-rails, graphiti_spec_helpers, and graphiti_errors are now part of graphiti itself. Remove them from your Gemfile after upgrading graphiti and everything keeps working. Rails integration is now opt-in per controller with include Graphiti::Rails::Controller, instead of every controller silently receiving it (#535). Exception handling is built on rescue_registry (#526), and spec helpers live at Graphiti::SpecHelpers with a new set of RSpec matchers for asserting resource attributes and relationships (graphiti-api/graphiti_spec_helpers#14).
This collapse was primarily motivated by the fact that 80-90% of graphiti users are using it with rails, and collapsing the surface area makes maintenance and understanding easier.
Persistence that matches your mental model
The model you inspect is the model that saves (#465, docs). Hooks and overrides see the same assigned model instance that gets persisted, instead of a parallel attributes hash that could drift from reality. around_persistence hooks receive that model, writable guards now receive the model and attribute name so authorization can consider the actual record (#511), and the resource carries its assigned model rather than threading it through override signatures.
Writable guards evaluate under the action being performed on every path, so writable: proc { current_action == :create } answers the same whether or not you inspected the model first.
Serialization wins
belongs_torenders relationship resource ids by default, solving an issue that had been open since 2019 (#167), withbelongs_to_resource_ids_by_defaultand a per-relationshipresource_idsoption to control it. The default comes without a performance cost, since linkage renders from foreign keys already on the record.Resource.wrapserializes models you fetched yourself, so you can use graphiti's serialization without going through its finders (#513).- A subclass redeclaring a relationship now reaches its own serializer.
- Sideloaded entities are deduplicated across include paths, so a record reached through two different associations renders once, complete. Previously only the first copy to serialize got its relationships, leaving
"data": nullholes in included (#476). - A relationship linkage no longer disappears when an unrelated include happens to share its name.
?include=positions.classificationused to blank the employee's own classification. - Deduplication no longer widens a deep filtered association.
?include=positions.employee.positions&filter[positions.rank]=1returned every position where the shallow path correctly returned one.
Concurrency
Concurrent sideload resolution is more reliable, with fixes for deadlocks and swallowed errors and new stress specs covering behavior under load. Request state now lives in fiber storage rather than thread locals, so context, transaction hooks and debug chunks survive into fibers a request spawns. That makes graphiti work under fiber based servers such as falcon (#540), and closes a case where a sideload resolving inside a fiber would wait on the bounded pool from a pool thread.
ActiveSupport::CurrentAttributes are carried onto pool threads, so a sideload resolving concurrently still sees Current.user and anything else your app sets per request. Previously that state was left behind on the request thread, and a resource reading it during a concurrent sideload saw nothing.
Performance
Serialization is faster and allocates less, with the biggest gains on simple requests. Every release records its object allocations so changes are visible over time, and rake performance:current reports them locally.
Guardrails and tooling
rake graphiti:auditscans your resources for latent issues.rails g graphiti:localewrites every error code graphiti renders into a locale file, ready to customize or translate.- New rake tasks for schema checks, so verifying a schema no longer means running the whole suite.
- Rendering a relationship the model doesn't define now raises
MissingRelationshipMethodinstead of failing obscurely. - The resource generator takes a
--controllername, so it no longer overwrites a controller you already have (#528). context_namespaceis nowcurrent_action, which says what it actually is.
A documentation site that lives with the code
The docs site was rebuilt, absorbed into this repository, and now publishes automatically as new versions are cut. Runtime error messages link to the new docs, and there's a dedicated 2.0 upgrade guide. Behavior and documentation updates can ship at the same time!
Modern platform
Requires Ruby 3.2+ and Rails 7.1+. Tested against Ruby 3.2 through 4.0 and Rails 7.1 through 8.1, on a rebuilt CI matrix and release pipeline. Apps on older Rubies and Rails stay supported on the 1.x maintenance branch.
Breaking changes
The upgrade guide walks through each of these with migration steps. The short version:
- Ruby 3.2+ and Rails 7.1+ are required.
- Controllers serving graphiti resources must
include Graphiti::Rails::Controller. Under graphiti-rails, every controller received the integration automatically (#535). around_persistencehooks receive the assigned model instead of the attributes hash (#465). Move attribute-hash edits tobefore_attributes, or set values on the model. Customcreate/updateoverrides keep their 1.x signatures and need no changes.include GraphitiErrorsraises instead of warning, because rescue_registry replaced it and there is nothing left to point it at.GraphitiErrors.enable!/disable!becomeshandle_request_exceptions, and 409 responses now report code "conflict" and title "Conflict Error".belongs_torelationships render resource linkage by default, so those relationship objects gain adatakey in payloads (#167). Setbelongs_to_resource_ids_by_default = falsefor 1.x output.- Relationships that render neither ids nor a link are omitted rather than rendered empty.
allow_nilanddeny_emptyfold into oneblanks:filter option, and the pagination settings are named after the page params they control.
Everything else from 1.x resolves with a deprecation warning and keeps working until 3.0.
Closes
- Closes #167 -
belongs_torenders resource linkage by default, from foreign keys already on the record so no extra queries - Closes #518 - the rebuilt docs site fixed the cheatsheet typo
- Closes #520 - generated specs call the helpers by their full names, and the
Sugaraliases are deprecated - Closes #526 - one exception-handling stack via rescue_registry,
register_exceptionbehaves as documented - Closes #528 -
--controllerlets a resource point at a controller without overwriting it - Closes #530 - superseded, Rails integration is now explicit opt-in rather than auto-include
- Closes #535 - controllers opt in with
include Graphiti::Rails::Controller(transferred from graphiti-rails#52) - Closes #540 - request state moved to fiber storage, so graphiti works under falcon and other fiber based servers
- Closes #524 -
includedalways renders when the client asked for it, so resolving the proxy first no longer drops sideloads - Closes #534 - registered exceptions render their message as
detail, and every code's text is overridable from a locale key - Closes #216 - error titles and details come from locale keys, with a generator that writes every code graphiti renders
- Closes #347 - invalid page parameters are rejected rather than raising NoMethodError
- Closes #199 - an empty polymorphic configuration is treated as unset
- Closes #476 - duplicate sideloaded entities now squash to one complete object; @factyy's diagnosis and approach landed here directly, since the PR had drifted from the 2.0 internals
- Closes #516 - pipeline, dependency, and linter updates cherry-picked from @caseyprovost's PR
Related work from the satellite gems
These PRs live in the now-absorbed repos and shaped what shipped here:
- graphiti-api/graphiti-rails#91 - rake task helpers out of the global namespace, landed as
Graphiti::Rails::RakeHelpers - graphiti-api/graphiti_spec_helpers#14 - RSpec matchers for attributes and relationships, reimplemented for 2.0 as
Graphiti::SpecHelpers::Matchers - graphiti-api/graphiti-rails#53 - a controller namespace option by @dineshpanda, superseded by
--controller, which names the controller outright rather than deriving it from.graphiticfg.yml