Skip to content

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 01 Sep 17:12
· 20 commits to main since this release

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_to renders relationship resource ids by default, solving an issue that had been open since 2019 (#167), with belongs_to_resource_ids_by_default and a per-relationship resource_ids option to control it. The default comes without a performance cost, since linkage renders from foreign keys already on the record.
  • Resource.wrap serializes 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": null holes in included (#476).
  • A relationship linkage no longer disappears when an unrelated include happens to share its name. ?include=positions.classification used to blank the employee's own classification.
  • Deduplication no longer widens a deep filtered association. ?include=positions.employee.positions&filter[positions.rank]=1 returned 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:audit scans your resources for latent issues.
  • rails g graphiti:locale writes 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 MissingRelationshipMethod instead of failing obscurely.
  • The resource generator takes a --controller name, so it no longer overwrites a controller you already have (#528).
  • context_namespace is now current_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_persistence hooks receive the assigned model instead of the attributes hash (#465). Move attribute-hash edits to before_attributes, or set values on the model. Custom create/update overrides keep their 1.x signatures and need no changes.
  • include GraphitiErrors raises instead of warning, because rescue_registry replaced it and there is nothing left to point it at. GraphitiErrors.enable!/disable! becomes handle_request_exceptions, and 409 responses now report code "conflict" and title "Conflict Error".
  • belongs_to relationships render resource linkage by default, so those relationship objects gain a data key in payloads (#167). Set belongs_to_resource_ids_by_default = false for 1.x output.
  • Relationships that render neither ids nor a link are omitted rather than rendered empty.
  • allow_nil and deny_empty fold into one blanks: 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_to renders 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 Sugar aliases are deprecated
  • Closes #526 - one exception-handling stack via rescue_registry, register_exception behaves as documented
  • Closes #528 - --controller lets 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 - included always 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: