protobuf icon indicating copy to clipboard operation
protobuf copied to clipboard

ruby: No way to access `oneof` field if it shares the name of a method on `Object`

Open jez opened this issue 7 months ago • 7 comments

If a oneof field shares the name of a Ruby method defined on Object or Kernel, there is no way to get the oneof field.

Normally, you do something like my_message.my_oneof to get the value. But for a proto definition like oneof method { ... }, that would mean doing my_message.method, which then attempts to call Kernel#method in the Ruby VM (e.g., to look up a method with the given name). That then fails, because method takes a required arg, but that's irrelevant: what it's not doing is looking up the oneof field named method on the message.

Normal, non-oneof fields work around this via the [] method on AbstractMessage, so that you can do my_message["method"]. That access pattern does not work when method is a oneof field instead of a normal field on the message.

Since protobuf allows oneof fields defined with arbitrary names (including names that collide with Ruby method names), there needs to be a way to access the field even in the presence of these collisions.

To my knowledge, there is no workaround for this in the mean time.

Update: https://github.com/protocolbuffers/protobuf/issues/21736#issuecomment-2881258582

What version of protobuf and what language are you using?

Language: Ruby

❯ bundle info google-protobuf
  * google-protobuf (4.30.2)
        Summary: Protocol Buffers
        Homepage: https://developers.google.com/protocol-buffers
        Source Code: https://github.com/protocolbuffers/protobuf/tree/v4.30.2/ruby
        Path: /pay/home/jez/sandbox/vendor/bundle/ruby/3.3.0/gems/google-protobuf-4.30.2-x86_64-linux

❯ bin/protoc --version
libprotoc 31.0-rc2

What operating system (Linux, Windows, ...) and version?

Linux (platform agnostic)

What runtime / compiler are you using (e.g., python version or gcc version)

Ruby 3.3, but should be version agnostic

What did you do?

Gemfile

source 'https://rubygems.org'

gem 'google-protobuf'

Gemfile.lock

GEM
  remote: https://rubygems.org/
  specs:
    bigdecimal (3.1.9)
    google-protobuf (4.30.2)
      bigdecimal
      rake (>= 13)
    google-protobuf (4.30.2-aarch64-linux)
      bigdecimal
      rake (>= 13)
    google-protobuf (4.30.2-arm64-darwin)
      bigdecimal
      rake (>= 13)
    google-protobuf (4.30.2-x86-linux)
      bigdecimal
      rake (>= 13)
    google-protobuf (4.30.2-x86_64-darwin)
      bigdecimal
      rake (>= 13)
    google-protobuf (4.30.2-x86_64-linux)
      bigdecimal
      rake (>= 13)
    rake (13.2.1)

PLATFORMS
  aarch64-linux
  arm64-darwin
  ruby
  x86-linux
  x86_64-darwin
  x86_64-linux

DEPENDENCIES
  google-protobuf

BUNDLED WITH
   2.5.22

test_message.proto

message TestMessageEither {
  oneof either {
    string left = 1;
    int32 right = 2;
  }
}

message TestMessageMethod {
  oneof method {
    string left = 1;
    int32 right = 2;
  }
}

test_message_pb.rb

# frozen_string_literal: true
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: test_message.proto

require 'google/protobuf'


descriptor_data = "\n\x12test_message.proto\">\n\x11TestMessageEither\x12\x0e\n\x04left\x18\x01 \x01(\tH\x00\x12\x0f\n\x05right\x18\x02 \x01(\x05H\x00\x42\x08\n\x06\x65ither\">\n\x11TestMessageMethod\x12\x0e\n\x04left\x18\x01 \x01(\tH\x00\x12\x0f\n\x05right\x18\x02 \x01(\x05H\x00\x42\x08\n\x06method"

pool = ::Google::Protobuf::DescriptorPool.generated_pool
pool.add_serialized_file(descriptor_data)

TestMessageEither = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("TestMessageEither").msgclass
TestMessageMethod = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("TestMessageMethod").msgclass

foo.rb

require 'google/protobuf'
require_relative './test_message_pb.rb'

def try(testcase)
  print(testcase)
  print(": ")
  begin
    yield
  rescue => e
    STDERR.puts("#{e.class}: #{e.message}")
    STDERR.puts(e.backtrace)
  end
  puts
end

puts "----- Expected behavior ------------------------------------------------"
msg = TestMessageEither.new(left: "hello")
try("msg") { p(msg) }
try("msg.left") { p(msg.left) }
try('msg["left"]') { p(msg["left"]) }
try("msg.either") { p(msg.either) }
try('msg["either"]') { p(msg["either"]) }

puts "----- Unexpected behavior ----------------------------------------------"
msg = TestMessageMethod.new(left: "hello")
try("msg") { p(msg) }
try("msg.left") { p(msg.left) }
try('msg["left"]') { p(msg["left"]) }
try("msg.method") { p(msg.method) }
try('msg["method"]') { p(msg["method"]) }

.ruby-version

3.3.5
bundle exec ruby foo.rb

What did you expect to see

No exception for the msg.method line, or at least something useful for the msg["method"] line.

What did you see instead?

❯ bundle exec ruby foo.rb
----- Expected behavior ------------------------------------------------
msg: <TestMessageEither: left: "hello">

msg.left: "hello"

msg["left"]: "hello"

msg.either: :left

msg["either"]: nil

----- Unexpected behavior ----------------------------------------------
msg: <TestMessageMethod: left: "hello">

msg.left: "hello"

msg["left"]: "hello"

msg.method: ArgumentError: wrong number of arguments (given 0, expected 1)
foo.rb:29:in `method'
foo.rb:29:in `block in <main>'
foo.rb:8:in `try'
foo.rb:29:in `<main>'

msg["method"]: nil

Anything else we should know about your project / environment

I have included a zipfile if you'd like to avoid copy/pasting a bunch of files:

test.zip

unzip test.zip
cd sandbox
bundle install
bundle exec ruby foo.rb

jez avatar May 13 '25 23:05 jez

Update: I was able to find a very gross workaround by calling method_missing directly:

message.method_missing(:method)

jez avatar May 14 '25 19:05 jez

Glad to know a workaround exists, but I agree this is not the intended behavior. What the intended behavior is might not be well specified yet, so give me a little time to research how we handle this edge case in other language bindings.

JasonLunn avatar May 14 '25 19:05 JasonLunn

We triage inactive PRs and issues in order to make it easier to find active work. If this issue should remain active or becomes active again, please add a comment.

This issue is labeled inactive because the last activity was over 90 days ago. This issue will be closed and archived after 14 additional days without activity.

github-actions[bot] avatar Sep 05 '25 10:09 github-actions[bot]

This is still relevant. The [] method on a message likely needs to change to accept oneof fields.

jez avatar Sep 05 '25 15:09 jez

We probably won't be able to get to this in the near term, but would accept a patch.

bellspice avatar Oct 28 '25 17:10 bellspice

We probably won't be able to get to this in the near term, but would accept a patch.

Sounds good, here you go: #24181

jez avatar Oct 28 '25 20:10 jez

Github actions closed the pr for unknown reasons, we reopened it and are waiting for a response.

PilgrimMemoirs avatar Nov 18 '25 18:11 PilgrimMemoirs