Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ActionController meets Protocol Buffers

ActionController meets Protocol Buffers

Avatar for Sorah Fukumori

Sorah Fukumori

October 16, 2026

More Decks by Sorah Fukumori

Other Decks in Programming

Transcript

  1. whoami • Sorah Fukumori (sorah) https://sorah.jp • IVRy Inc. Principal

    Software Engineer • Ex: Cookpad Inc. (-2025-03) • RubyKaigi Organizer • RubyKaigi Wi-Fi NOC
  2. AD: RubyKaigi NOC is providing Wi-Fi here • Around 950

    clients, 300Mb-500Mbps traf c • Dashboard: https://grafana.rubykaigi.net/ • Hashtag: #rubykaigiNOC fi Claude just joined our team to constantly monitor and remedy the things…
  3. AD 2: RubyMusicMixin tonight • RubyMusicMixin on Rails 2026 •

    Hosted by pixiv × IVRy • 8pm TONIGHT • Leave and come back any time (1 drink required) • Just drop by and soak in some music 0w0.co/kor26
  4. What I've been working on lately 0w0.co/kor26 • Surprisingly, I'm

    writing user-facing Rails • after years of "what is this person even doing for work", my previous job included • Login, session management, an OAuth authorisation server, authorisation, and so on • Hiring: SaaS Control Plane engineers, Security engineers • Authentication, authorization, billing, contract management, etc…
  5. 0w0.co/kor26 rails new • So I did rails new for

    the rst time in a while • Split out of the monolith, anticipating going multi-product • Also an exploration of our baseline development style from now on, infra included • OpenAPI stands in the way • Our existing monolith Rails app shares its schema with the frontend and mobile apps via OpenAPI fi • Even if an LLM writes it for you, nobody wants to review this
  6. Nobody wants to review this, right? /admin/shops/{shop_id}/items/{id}: patch: tags: -

    admin - shopItem description: | Updates the display SKU of a shop's item. Used to link the storefront SKU for items whose warehouse code differs from the storefront SKU. operationId: updateShopItemForAdmin security: - Bearer: [] parameters: - in: path name: shop_id schema: type: integer required: true description: Shop ID - in: path name: id schema: type: integer
  7. type: object additionalProperties: false properties: display_sku: type: string nullable: true

    description: | SKU shown on the storefront. Not updated when omitted. An empty string or null unlinks it. example: "RK-2026-0001" responses: "200": description: ok content: application/json: schema: $ref: "../../../components/ShopItemForAdmin.yaml#/components/schemas/ShopItemForAdmin" "400": description: Validation error content: application/json: schema: $ref: "../../../components/ValidationError.yaml#/components/schemas/ValidationError" "404": description: Resource not found content: application/json: schema: Nobody wants to review this, right?
  8. The same API in Protobuf service ShopAdminService { // Updates

    the display SKU of a shop's item. // Used to link the storefront SKU for items whose warehouse code differs from the storefront SKU. rpc UpdateShopItem(UpdateShopItemRequest) returns (ShopItemForAdmin) { option (google.api.http) = { patch: "/admin/shops/{shop_id}/items/{id}" body: "body" }; option (gnostic.openapi.v3.operation) = {tags: ["admin", "shopItem"]}; } } message UpdateShopItemRequest { int64 shop_id = 1 [(buf.validate.field).int64.gt = 0]; // Shop ID int64 id = 2 [(buf.validate.field).int64.gt = 0]; // Item ID Body body = 3 [(buf.validate.field).required = true]; message Body { // SKU shown on the storefront. // Not updated when omitted. An empty string unlinks it. optional string display_sku = 1 [(buf.validate.field).string = { max_len: 32
  9. service ShopAdminService { // Updates the display SKU of a

    shop's item. // Used to link the storefront SKU for items whose warehouse code differs from the storefront SKU. rpc UpdateShopItem(UpdateShopItemRequest) returns (ShopItemForAdmin) { option (google.api.http) = { patch: "/admin/shops/{shop_id}/items/{id}" body: "body" }; option (gnostic.openapi.v3.operation) = {tags: ["admin", "shopItem"]}; } } The same API in Protobuf message UpdateShopItemRequest { int64 shop_id = 1 [(buf.validate.field).int64.gt = 0]; // Shop ID int64 id = 2 [(buf.validate.field).int64.gt = 0]; // Item ID Body body = 3 [(buf.validate.field).required = true]; message Body { // SKU shown on the storefront. // Not updated when omitted. An empty string unlinks it. optional string display_sku = 1 [(buf.validate.field).string = { max_len: 32 pattern: "^([A-Z0-9-]+)?$" }]; } }
  10. Looking for types and API schema de nitions • Our

    project is an IAM platform where a system I want to write robustly, so I want types. However I want Rails too, as our team is used to & for exibility. • It has plenty of an ordinary web app's side as well, so writing it all in Rust doesn't t • Considered OpenAPI, Protobuf, Smithy and more, then decided to give Protobuf a go • I've liked Protobuf for a long time • Used it for ISUCON10 too: its portal was a Protobuf-enabled Rails app But this time, I’d like it to be more integrated with Rails. fi fi fi fl • https://github.com/isucon/portal https://github.com/isucon/isucon10- nal
  11. Generated vs hand-written API schemas • I did consider whether

    we could get by with OpenAPI • though it's painful to both write and read • A fair number of people generate it through tests, right? • RSwag, Typelizer, and so on • …Isn't the reason you want to generate it that writing it is painful?
  12. Generated vs hand-written API schemas (2) • Because it is

    a protocol, it's better to reach some agreement with the API consumer side • Then discussing over a hand-written schema works best • But OpenAPI/Swagger YAML is hard for humans to read and write • With LLM-assisted development, schema de nitions matter even more • Handy for conversations like "an API with this schema would be nice, I want something like this” fi fi • In the end, I gave up on auto-generated schema and made Protobuf- rst
  13. Yes, itʼs now a Rails app with no bare hashes

    • Today's talk is about that Rails app • Excluding con g, Rails APIs and kwargs-ish ones, apparently only around 50 • About 350 for Rails APIs fi • About 300 constructions of Data classes and protobuf messages • 180 message types, 78 Data classes, 0 Structs • LOC 17071, 968 methods
  14. Agenda • Protocol Buffers 101 • ActiveRecord and Protobuf •

    ActionController and protobuf • Protovalidate • Connect RPC • Pain points
  15. Open Sourced Gems • Extracted concerns from that Rails app

    to gems! • https://github.com/ivry-inc/connect_rpc_rails • https://github.com/sorah/protobufable • https://github.com/sorah/problem 0w0.co/kor26
  16. Demo branch: sponsor-app • https://github.com/ruby-no-kai/sponsor-app/tree/protobuf/docs • Here's an example of

    implementing all of topics covered today on sponsorships.rubykaigi.org (vibe coded) • No time for a detailed demo, though… • Code excerpts sometimes omit part of module names 0w0.co/kor26
  17. So, what is Protocol Buffers? • A schema de nition

    language and its serialisation format • Made by Google, Open-sourced in 2008. • Around long before gRPC (2015) • De ne messages in .proto, generate code, and use it • The binary representation is small. Notably, eld names aren't in fi fi fi fi it; it uses the tag numbers from the schema de nition instead
  18. Code generators • Besides built-in generators like ruby_out, it can

    also call external executables to generate additional code • Such as actual API client/server codes like gRPC fi • rbs_out, nally merged recently, can output RBS too! • Though Tapioca's Protobuf support is more powerful
  19. Ruby looks like this • proto/rubykaigi/sponsor_app/admin_plans.proto message AdminPlan { int64

    id = 1; string name = 2; // Display name, such as "Ruby" or "Platinum". int32 rank = 3; optional string summary = 4; // One line describing the plan. int32 capacity = 8; optional google.protobuf.Timestamp closes_at = 14; }
  20. Ruby looks like this (2) plan = Rk::AdminPlan.new(name: "Ruby", capacity:

    3) plan.name #=> "Ruby" plan.rank #=> 0 plan.rank = "1" #=> Google::Protobuf::TypeError #=> ArgumentError: Unknown field name 'nmae' Rk::SponsorApp::AdminPlan.new(nmae: “Ruby") bin = Rk::AdminPlan.encode(plan) p Rk::AdminPlan.decode(bin) == plan #=> true
  21. Using it with ActiveRecord Letʼs start somewhere easy • Reminder:

    protobuf is a typed data format • so Serialization/Deserialization is sort of its main job • Who has used serialize? The one that turns things into YAML or JSON • serialize :…, coder: JSON • Ever been frustrated by not having types?
  22. Protobufable::JsonType, BinaryCoder • Time for the Protobufable gem • Binary

    or JSON? may be a better deal since you can use SQL functions and such # binary column serialize :settings, coder: Protobufable::BinaryCoder.new(Rk::FallbackOptions) # json column attribute :settings, Protobufable::JsonType.new(Rk::FallbackOptions), default: -> { Rk::FallbackOptions.new }
  23. You get handy accessors class FormDescription < ApplicationRecord attribute :fallback_options,

    Protobufable::JsonType.new(Rk::FallbackOptions, preserve_proto_fieldnames: true), default: -> { Rk::FallbackOptions.new } end desc = FormDescription.first p desc.fallback_options #=> #<Rk::FallbackOptions …> desc.fallback_options.options.map(&:name) #=> ["Gold", ...] desc.fallback_options.options << Rk::FallbackOption.new(value: "gold", name: "Gold") desc.save!
  24. Let's use it for REST APIs too • It's already

    handy enough for internal use, ActiveRecord models included, but Protobuf is a data interchange format. • Let's use it for standard REST APIs implemented in Rails • Protobuf isn’t gRPC exclusive!
  25. Generate OpenAPI schema using Protobuf • Because we already had

    a frontend codebase that established OpenAPI based work ow, our Rails app has to accept OpenAPIshaped API requests. • I didn’t think it’s ef cient to change everything at the same time, thus I explored how we can provide openapi.yml from Protobuf- rst app. • There are multiple protoc plugins to emit OpenAPI schema from fl fi fi fi Proto les.
  26. Protobuf service de nition w/ Gnostic service ShopAdminService { rpc

    UpdateShopItem(UpdateShopItemRequest) returns (UpdateShopItemResponse) { option (google.api.http) = { patch: "/admin/shops/{shop_id}/items/{id}" body: "body" }; option (gnostic.openapi.v3.operation) = { tags: "admin" }; } } fi message UpdateShopItemRequest { int64 shop_id = 1; int64 id = 2; Body body = 3; message Body { string display_sku = 1; } }
  27. google/gnostic • We chose gnostic for the best exibility and

    OpenAPI v3 support fi fi fi fl • Protobuf’s option is very powerful feature • You can attach annotation data to les, messages and elds • json_name mentioned earlier is one of the built-in options • gnostic uses options to inject OpenAPI-speci c data
  28. Filling the gap • gnostic assumes google.rpc.Status for error responses,

    which didn't match our existing API design • For that part, we patch protoc's output and use the result as the nal openapi.yml • And nally, the generated schema is veri ed for correctness in request specs fi fi fi fi • https://github.com/ahx/openapi_ rst
  29. How do you share API schemas? • Whether OpenAPI or

    Protobuf, sharing between systems comes with a lot of hassle • Typical approaches • monorepo: has monorepo-speci c pains, but seems handy and easy fi • package (gem): releases and versioning are a hassle • submodule: pulling and updating are both a pain
  30. IVRy has a submodule…… • Since I joined, IVRy has

    had a schema repo sitting in between of frontend and backend repositories • And we still have. • Getting the schema reviewed and merged rst, then hitting rework on the backend side and waiting for review again: that kind of hassle tends to happen • Having to nish the merge on the schema side and update the submodule… before the backend side can merge is tough • Ideally, the frontend and backend of the same system seem happier in the fi fi same repo
  31. Now we just copy it • Starting at our new

    Rails app, we established a mechanism that sends pull requests to the dependent repos when openapi.yml or proto is updated • Hint: Obtain GitHub Apps installation tokens for crossing repos in GitHub Actions with ID tokens • https://github.com/sorah/sts-cat allows safely exchanges ID tokens for installation tokens!
  32. We commit generated les • We commit all generated code

    so we can tell what to copy • Whether to commit or not seems to be a matter of style • Commit: works right after clone, so fewer steps to get it running. Also usable to decide what to copy to other repos • Don't commit: diffs bloat fi fl • CI work ow runs all generate tasks and ensure there’s no diff
  33. .gitattributes linguist-* • A git attribute used by https://github.com/github/linguist •

    linguist-vendored marks les as vendored, • linguist-generated marks les as generated • They're collapsed by default in PR diffs, so set them when doing fi fi this kind of thing
  34. Sharing schemas • Whether or not we keep the schema

    repo itself, we plan to lean towards copying into the schema repo and each repo • There's also the Buf Schema Registry from Buf, so that might be an option once protos pile up
  35. API schemas alone aren't enough • Now we understand we

    can interop with OpenAPI-expecting API clients. • However, just generating a openapi.yml is no different from merely having an openapi.yml • We need to properly use generated Protobuf message classes in the actual API implementation
  36. Deserialize - Protobufable::RequestParseable • A concern that decodes a JSON

    body into the protobuf message class class ThingsController < … speci ed for the action and expose via include Protobufable::RequestParseable message helper protobuf_body Rk::PatchThingRequest • Also copied into params[*], so logging def update & lter_attributes just work p message #=> #<Rk::PatchThingRequest> # ... • Annotation is saved to a controller ivar. end end • ActionDispatch::Request is monkey fi fi patched to hijack params_parser.
  37. Serialize - Protobufable::AlbaBinding https://github.com/okuramasafumi/alba • Adopted Alba as a modern

    serializer, but making it emit Protobuf instead of JSON • Use Alba's attribute declarations and such inside the with_protobuf helper • Error when a nonexistent eld is speci ed • Error when a eld is missing • Nesting with one, many DSL just works as usual fi fi fi • You use as_protobuf, instead of as_json and to_json.
  38. Serialize - Protobufable::AlbaBinding https://github.com/okuramasafumi/alba class AdminPlanResource include Alba::Resource include Protobufable::AlbaBinding

    with_protobuf Rk::AdminPlan do attributes :id, :name, :rank, :capacity attributes :summary, if: proc { |plan| plan.summary.present? } attributes closes_at: :timestamp, if: proc { |plan| plan.closes_at } attribute(:price) { |plan| plan.price.to_s } end end
  39. Protobufable::Renderers • The railtie automatically installs proto: and proto_json: renderer

    so you just pass a protobuf message object to render it. render proto_json: GetAdminPlanResponse.new( plan: AdminPlanResource.new(plan).as_protobuf )
  40. Problem gem https://github.com/sorah/problem • A gem that allows rendering errors

    in the RFC 9457 Problem Details format and provides a concern for Exception to de ne error types • Not Protobuf-aware by itself class Error < StandardError include Problem::Detailable end class NotAllowed < Error type "forbidden" status 403 title "not allowed" end fi class ApplicationController < … include Problem::Rescuable def index = raise NotAllowed end
  41. Managing errors in Protobuf de nitions •Protobufable provides an extension

    to the Problem gem to give API consumers an error schema •The app's proto de nitions hold errors as an enum, and enum value options set type and status •This allows API consumers to understand error types in advance, e.g. help generating error classes at client side. •Also this bene ts the API schema review, because reviewers can take a look at added error types at the same time. •And for security reasons, APIs can choose the same error type across multiple error types to hide a actual reason. But internal logs can tell them apart by the enum value. fi fi fi fi Since we ended up adding Connect RPC in the end anyway, the theory that google.rpc.Status everywhere from the start would've been ne, well, isn't entirely wrong…
  42. problem.proto • Declare the uri and HTTP status with an

    enum value option extend google.protobuf.EnumValueOptions { optional protobufable.ProblemTypeOptions problem_type = 50001; } enum ProblemType { PROBLEM_TYPE_UNSPECIFIED = 0; } // The resource the request names does not exist. PROBLEM_TYPE_NOT_FOUND = 3 [(problem_type) = { uri: "not-found" http_status: 404 }];
  43. Binding error classes to the enum • Error classes on

    the Ruby side just point at an enum value module Errors class ApiError < StandardError include Protobufable::Problem::Typeable def self.problem_type_enum = 'rubykaigi.sponsor_app.ProblemType' def self.problem_type_annotation = 'rubykaigi.sponsor_app.problem_type' end class NotFound < ApiError problem_type :PROBLEM_TYPE_NOT_FOUND # type and status is loaded from Protobuf enum descriptor end end raise Errors::NotFound.new(detail: "No conference has the slug #{slug.inspect}.") # => {type: "https://sponsorships.rubykaigi.org/problems/not-found", status: 404}
  44. The usual NoMethodError in Rails • params[...] being nil, or

    Array due to crafted requests • Every Rails developer must have seen this kind of error • Validating parameters seriously in Rails is a hassle, or rather, it happens because it's a hassle def update @item.update!(name: params[:item][:name].strip) end # {"item": null} # {"item": {"name": 1}} # {"item": []} => NoMethodError: undefined method '[]' for nil => NoMethodError: undefined method 'strip' for an instance of Integer => TypeError: no implicit conversion of Symbol into Integer
  45. Protovalidate • A Protobuf library made by Buf • Set

    of *.proto schema of an extension, and runtime validator • De ne validation rules on messages and elds as options • Rules are attached to the schema, so not only API producers but API consumers can use it too • Complex rules can be written in CEL fi fi • CEL: a rule language made by Google
  46. Protovalidate rules • When the built-in rules aren't enough, write

    a CEL expression • Runtimes that evaluate built-in-only rules fast without CEL are starting to appear, so those seem preferable in general message Condition { option (buf.validate.message).cel = { id: "fallback_option.condition.not_empty" message: "a condition must name plans or a booth request" expression: "size(this.plans) > 0 || has(this.booth_request)" }; } repeated string plans = 1 [(buf.validate.field).repeated = { unique: true items: {string: {min_len: 1, max_len: 100}} }]; optional bool booth_request = 2;
  47. protovalidate-rb https://github.com/sorah/protovalidate-rb • A binding of protovalidate-cc (through a Rust

    shim) • This approach is a copy of protovalidate-py, but -py has ended up carrying a Rust implementation inside • That's the move to the fast runtime that skips CEL where CEL isn't needed, mentioned earlier
  48. Protobufable::RequestValidatable • Used on top of Protobufable::RequestParseable • Validates requests

    with Protovalidate on its own and returns errors • Behaviour is customisable by rescue_from-ing InvalidMessage • By default, 400 through config.action_dispatch.rescue_responses
  49. class FallbackConditionsController < ApplicationController include Protobufable::RequestValidatable rescue_from Protobufable::RequestValidatable::InvalidMessage do |e|

    render status: :bad_request, json: {errors: e.violations.map(&:to_s)} end protobuf_body Condition def create # Requests reach here guaranteed to satisfy validation rule end end # {} => 400 {"errors": ["a condition must name plans or a booth request [fallback_option.condition.not_empty]”]} # {"plans": ["Gold", "Gold", ""]} # => 400 {"errors": [ # "plans: repeated value must contain unique items [repeated.unique]", # "plans[2]: must be at least 1 characters [string.min_len]”]}
  50. Doing an RPC server with Rails • There are plenty

    of moments where RPC would do instead of REST • With Connect RPC or gRPC, you can use plenty of excellent client libraries and their code generators • I want ActionController, but it doesn't have to be REST • With only what the grpc gem provides, there are none of the handy features, before_action included • If we could keep the ActionController feel and reuse assets like concerns that REST APIs use too… that'd be great!
  51. What is gRPC? • An RPC framework made by Google

    • Generates client and server stub code from the proto service de nition • Exchanges Protobuf binaries over HTTP/2 • Has a wide variety of built-in features, streaming APIs included • HTTP/2-based, but carrying the status in an HTTP Trailer is a distinctive trait, or rather its only awkward part • There are various reasons, like the status might change while generating the response • Isn't most of so-called load balancer gRPC support about this? [citation needed] fi • FIXME: restore monospace: service
  52. grpc gem is painful • Both Server and Client have

    interfaces far from the Ruby/Rails feel • Well, it's a C++ binding…… • And the codebase is ridiculously large, clearly overkill
  53. Connect RPC • Rationale: https://buf.build/blog/connect-a-better-grpc • Says: “grpc frameworks are

    overkill; I get the protocol, but simple is better” • So pain is not just in Ruby • Simpler than gRPC-Web • Simpli ed down to being as easy as using a regular REST API with curl fi Although connect-go library provides simple gRPC protocol implementation
  54. Using with Curl is Easy Because itʼs simple • Just

    POST some JSON. • RPCs declared side-effect free can be called with GET too curl -H 'Content-Type: application/json' -H 'Connect-Protocol-Version: 1' \ -d '{"conferenceSlug": "rubykaigi2048"}' \ http://localhost:3000/rubykaigi.sponsor_app.AdminService/GetConference curl 'http://localhost:3000/rubykaigi.sponsor_app.AdminService/GetConference' \ '?connect=v1&encoding=json&message=%7B%22conferenceSlug%22%3A%22rubykaigi2048%22%7D'
  55. connect̲rpc̲rails https://github.com/ivry-inc/connect̲rpc̲rails • Built. • Actually my colleague @draftcode had

    the motivation, and I had kept injecting suggestions again again and again from the side to make it Rails-ish
  56. routes.rb - connect̲service • Assign rpcs to each controller with

    the connect_service DSL • GET/POST routes following the Connect RPC protocol are generated behind the scenes # config/routes.rb connect_service 'rubykaigi.sponsor_app.AdminService' do rpc 'ListConferences' => 'api/admin/conferences' rpc 'GetConference' => 'api/admin/conferences' rpc 'UpdateConference' => 'api/admin/conferences' rpc 'GetPlan' => 'api/admin/plans' end
  57. routes.rb - connect̲service • By the way, like gRPC, it

    should be about 1 server 1 service • because code generation adds that many client classes • The whole Rails app is 1 service, but rpcs can be scattered across controllers # config/routes.rb connect_service 'rubykaigi.sponsor_app.AdminService' do rpc 'ListConferences' => 'api/admin/conferences' rpc 'GetConference' => 'api/admin/conferences' rpc 'UpdateConference' => 'api/admin/conferences' rpc 'GetPlan' => 'api/admin/plans' end
  58. Controller • The rest is just writing it normally. message

    gives you the request as a protobuf message. • Protovalidate is supported too! class Api::Admin::ConferencesController < Api::Admin::ApplicationController def update_conference conference = Conference.find_by!(slug: message.conference_slug) save_input!(conference, connect_request.input) Rk::AdminServiceUpdateConferenceResponse.new( conference: admin_conference_message(conference) ) end end
  59. gRPC? • By using Falcon, I believe we can route

    unary gRPC request into ActionController but haven’t yet tried • We reached consensus to choose Connect RPC
  60. Hard to type helper methods within an action • Both

    Protobufable and ConnectRpcRails bring typed request params • But "within this action, the message helper returns this" can't be expressed in RBS or RBI…. • It's hard to express that message is this type in the call stack under def index • # self as ... & ... or m = message #: ... is messy because same type name is repeatedly shown • Helper methods can be called from multiple actions, so they can be ignored fi in the rst place. But we can’t even narrow down only within an action
  61. class TheController protobuf_body Foo def nah p message # <=

    inferred as Foo|Bar, not Foo # so we need annotation, repetitive! msg = message #: Foo end # Clean but can’t put this in RBI/RBS so Tapioca cannot help protobuf_body Bar def neh #: self as TheController & ProtobufBody[Bar] message # => Bar end end
  62. Doesn't play well with hot reload • The google-protobuf gem

    is a C++ binding, and schemas are managed by registering messages in a process-global descriptor pool • Code generated by ruby_out also just has a binary string written in it, feeds that to google-protobuf, pulls Protobuf message classes out of the native extension and stuffs them into constants • This is what doesn't allow hot reload…… • Putting up with it using watchexec
  63. Build system with bufbuild/buf • No need to work hard

    running protoc by hand. It’s a hard way • Use buf instead • It manages schema dependencies too • well known types, common types; • schemas used by other Protobuf-related libraries; • and even your internal ones (with appropriate registry setup)
  64. Well known types (WKT) • Lots of utility types provided

    out-of-the-box • such as Timestamp, Duration and Any • Whilst these aren’t a primitive type, but Protobuf language bindings often have type converters into each language's corresponding types • Though Google::Protobuf::Timestamp conversion is not AS::TimeWithZone aware…
  65. ProtoJSON • It actually has a JSON representation • Defaults

    to camelCase without option • Some WKTs have a JSON representation too • Timestamp: RFC 3339 • Duration: “90s” • Also handy for parsing JSON from outside while type-checking it AdminPlan.encode_json(plan) #=> ‘{“name":"Ruby", # "closesAt":"2026-10-15T10:00:00Z"}' AdminPlan.encode_json( plan, preserve_proto_fieldnames: true) #=> ‘{"name":"Ruby", "closes_at":"2026-10-15T10:00:00Z"}' AdminPlan.decode_json('{"name": 1}') #=> Google::Protobuf::ParseError: Error parsing JSON # @1:10: Expected string
  66. ProtoJSON eld names • Even overridable via `json_name` option •

    Decode accepts any possible eld names anyway message Plan { string price_text = 1; string price_booth = 2 [json_name = "price_booth"]; } // => "priceText" // => "price_booth" fi fi // Plan.encode_json(plan) // #=> {"priceText":"3,000,000 JPY","price_booth":"500000"} // Plan.encode_json(plan, preserve_proto_fieldnames: true) // #=> {"price_text":"3,000,000 JPY","price_booth":"500000"}
  67. ProtoJSON: Cons • https://protobuf.dev/best-practices/dos-donts/#text-formatinterchange says: • If you use protos

    converted to JSON in your API or for storing data, you may not be able to safely rename elds or enums at all. fi fi • Acceptable as long as you don't rename elds
  68. Use it like a dataclass • Anyone who likes Data.de

    ne or Struct.new? I do. • Data class vs Protobuf message • Data: can take any objects. Immutable. But no inline typing support. • Protobuf: take only supported types, but assigning a wrong type errors immediately. • It's pretty handy as a dataclass replacement • and Protobuf wins once SerDe is required. • Types are nice, but properly de ned keys alone are already a win. A nonexistent fi fi attribute properly raises NoMethodError……
  69. Q. But I heard there's no nullable? Like Go, missing

    values become 0 or "", right? • A. Actually there's optional these days • but default values are less error prone than nil, which is better • even with optional, it returns the default value and only tells you whether the value is set • # Hash: nil when the key is missing hash[:summary] #=> nil # Protobuf: gets default value even with optional plan = Rk::SponsorApp::AdminPlan.new plan.summary #=> "" plan.has_summary? #=> false * Protobuf Editions, the version after proto3, is out and makes the optional equivalent the default (2023 onwards), but toolchain support may not be great yet)
  70. AIP always help us “Google API Improvement Proposals” • https://google.aip.dev/

    • Something like Google's API style guide • It comes from the company where Protocol Buffers and gRPC originated, so it's easy to take as a reference • It's full of guidelines that are useful as plain API design theory, more than about how to use Protobuf • And https://github.com/googleapis has lots of .proto les for public APIs fi like Google Cloud, worth a look too.
  71. Outro • Protobuf isn't limited to gRPC; it's easy to

    use as a schema de nition language • The Protobufable gem brings Protobuf types everywhere, from ActiveRecord json columns to REST API requests/responses, errors and validation • With gnostic and the like, interop with OpenAPI clients is possible too • We could also implement Connect RPC by making use of ActionController and the Rails ecosystem • Acknowledgements: this work includes plenty of ideas from my colleague @draftcode fi • AD: We’re hiring! Come visit RubyMusicMixin tonight 8pm. • Questions? Either JA/EN ok
  72. AD 2: RubyMusicMixin tonight • RubyMusicMixin on Rails 2026 •

    Hosted by pixiv × IVRy • 8pm TONIGHT • Leave and come back any time (1 drink required) • Just drop by and soak in some music 0w0.co/kor26
  73. AD 3: IVRy is hiring 0w0.co/kor26 • Relevant positions open:

    • Platform Engineer • Software Engineer (SaaS Control Plane) • Product Security Engineer • Authentication, authorisation, billing, contract management, etc…
  74. Outro • Protobuf isn't limited to gRPC; it's easy to

    use as a schema de nition language • The Protobufable gem brings Protobuf types everywhere, from ActiveRecord json columns to REST API requests/responses, errors and validation • With gnostic and the like, interop with OpenAPI clients is possible too • We could also implement Connect RPC by making use of ActionController and the Rails ecosystem • Acknowledgements: this work includes plenty of ideas from my colleague @draftcode fi • AD: We’re hiring! Come visit RubyMusicMixin tonight 8pm. • Questions? Either JA/EN ok