0.0
The project is in a healthy, maintained state
Parses JSON request bodies into generated protobuf message classes, stores messages in json and jsonb columns, catalogues RFC 9457 problems in a protobuf enum, and binds Alba serializers to the messages they produce.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 7.1
~> 13.0
~> 4.0
~> 0.14
~> 3.13
~> 1.82.0
~> 2.0
~> 2.0
~> 0.9

Runtime

 Project Readme

protobufable: Protocol Buffers as the schema of record for a Rails API

protobufable makes generated protobuf message classes the shape of a Rails application's data, on the way in and on the way out. A JSON request body is decoded straight into the message its action declares, so a wrong type is a 400 before the action runs and the action reads a typed object instead of params. A jsonb column holds a message whose shape is declared in a .proto. Errors take their type and status from a protobuf enum. Responses are encoded by the schema's own encoder.

Installing the gem switches on only the proto_json and protobuf renderers and the request body parser, which leaves a request alone unless its action declares protobuf_body. Every other piece is a concern you include into the classes that want it, and it acts on those classes only; each section below starts from that setup. The gem's only hard dependencies are google-protobuf and Rails; the integrations with problem, protovalidate, alba and connect_rpc_rails are separate requires, and none of those gems is a dependency.

class WidgetsController < ApplicationController
  include Protobufable::RequestParseable

  protobuf_body Api::CreateWidgetRequest
  def create
    widget = Widget.create!(name: message.name)
    render proto_json: Api::CreateWidgetResponse.new(widget: WidgetResource.new(widget).as_protobuf)
  end
end
$ curl -sS -X POST -H 'Content-Type: application/json' -d '{"name": 42}' https://api.example.com/v1/widgets
HTTP/1.1 400 Bad Request

Features

Request bodies

protobuf_body declares the message an action accepts. The JSON body is decoded into it before the action runs, so a wrong type or an unknown field is a 400 and the action reads a typed message instead of params. See docs/request-bodies.md.

class WidgetsController < ApplicationController
  include Protobufable::RequestParseable

  protobuf_body Api::CreateWidgetRequest
  def create
    Widget.create!(name: message.name, tags: message.tags.to_a)
  end
end

Responses

render proto_json: and render protobuf: encode a message with the schema's own encoder, in any controller. An Alba resource bound to a message fails at load time when the two disagree, rather than silently sending an empty field. See docs/responses.md.

require "protobufable/alba_binding"

class WidgetResource < ApplicationResource
  include Protobufable::AlbaBinding

  with_protobuf Api::Widget do   # raises at load time unless it matches Api::Widget
    attributes :id, :name
  end
end

render proto_json: Api::GetWidgetResponse.new(widget: WidgetResource.new(widget).as_protobuf)

Messages in a column

Protobufable::JsonType keeps a message in a json or jsonb column, declared per attribute. It is an ActiveModel type, not a serialize coder, so dirty tracking and casting behave. For a text or binary column, Protobufable::JsonCoder and Protobufable::BinaryCoder are coders for serialize. See docs/columns.md.

class Widget < ApplicationRecord
  attribute :settings, Protobufable::JsonType.new(Internal::WidgetSettings),
    default: -> { Internal::WidgetSettings.new }
end

Errors

An error catalogue in the schema, built on problem: type and status come from an enum value's annotation, so two errors needing different statuses are different types, and a typo in the catalogue fails on boot. See docs/errors.md.

enum ProblemType {
  PROBLEM_TYPE_UNSPECIFIED = 0;
  PROBLEM_TYPE_NOT_FOUND = 1 [(problem_type) = {uri: "not-found", http_status: 404}];
}
require "protobufable/problem"

class ApiError < StandardError
  include Protobufable::Problem::Typeable

  def self.problem_type_enum = "myapp.api.ProblemType"
  def self.problem_type_annotation = "myapp.api.problem_type"
end

class NotFound < ApiError
  problem_type :PROBLEM_TYPE_NOT_FOUND
end

class ApplicationController < ActionController::API
  include Problem::Rescuable   # from problem; renders them as application/problem+json
end

raise NotFound.new(detail: "no widget w_123")   # 404, "type": "not-found"

Value validation

buf.validate rules run on request bodies and on stored columns, each opted into separately. A request breaking a rule is a 400 before the action runs. See docs/validation.md, which also covers reporting the failed fields with field paths in the spelling the caller sent.

message CreateWidgetRequest {
  string name = 1 [(buf.validate.field).string = {min_len: 1, max_len: 100}];
}
require "protobufable/protovalidate"

class WidgetsController < ApplicationController
  include Protobufable::RequestValidatable   # brings RequestParseable

  protobuf_body Api::CreateWidgetRequest
  def create = ...
end

class ApplicationRecord < ActiveRecord::Base
  include Protobufable::ColumnValidatable
end

Connect RPC

A problem raised in a Connect RPC goes out as a Connect error carrying the same document, so a caller dispatches on the same type over REST and Connect. See docs/connect.md.

require "protobufable/connect"

class WidgetsController < ActionController::API
  include ConnectRpcRails::Controller
  include Protobufable::Connect::ProblemRescuable   # after ConnectRpcRails::Controller
end
{"code": "not_found", "message": "no widget w_123",
 "details": [{"type": "protobufable.ProblemDetails", "value": "..."}]}

Typed

RBS signatures ship with the gem, and the suite type checks under Steep.

Requirements

  • Ruby 3.4 or later
  • Rails 7.1 or later
  • google-protobuf 4.26 or later

Optional dependencies are not installed for you. Add the ones for the features you use:

Installation

bundle add protobufable

Rails wires itself up through a railtie, which registers the renderers, installs the request body parser, and lists a broken validation rule as a 400 in rescue_responses. Outside Rails, call Protobufable.install! at boot.

Caveats

  • protovalidate is a prerelease (0.1.0.beta3 at the time of writing).
  • Editing a .proto does not hot-reload. google-protobuf raises on a duplicate descriptor definition, so the development loop is to restart on regeneration.

Caveats specific to a feature are on its page, such as vendoring the shipped .proto files in docs/errors.md.

Development

bundle install
bundle exec rake      # specs, then rbs and steep
bundle exec yard      # docs
hk check --all        # rubocop, actionlint, zizmor
mise run protoc       # regenerate the spec fixtures

See also

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/sorah/protobufable.

License

The gem is available as open source under the terms of the MIT License. Copyright (c) 2026 Sorah Fukumori.