question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Improve support for `oneof` fields

See original GitHub issue

After using protobuf-es-generated code a bit more, I’m going to expand on my comment in another issue and make it an issue of its own.

Currently, accessing oneof fields in code generated by protobuf-es is a bit more cumbersome than in other protobuf code generators. In my protobuf definition I’ve got a oneof that might be a post, comment or profile. Both protoc-gen-ts in TypeScript and rust-protobuf provide ways to quickly access or set a .post. So in TypeScript I can do something like:

let post = item.post
if (post) { 
   // ... 
}

or

item.post = new Post(...)

But in code generated by protobuf-es, I have to do:

let post: Post|undefined = undefined
let it = item.itemType
if (it.case == "post") { post = it.value }
if (post) {
   // ...
}

which ends up in my code so often that I made a helper function for myself.

// Helper function for getting inner Item types.
export function getInner(item: pb.Item, field: "post"): pb.Post | undefined;
export function getInner(item: pb.Item, field: "profile"): pb.Profile | undefined;
export function getInner(item: pb.Item, field: "comment"): pb.Comment | undefined;
export function getInner(item: pb.Item, field: "post"|"profile"|"comment"): pb.Post | pb.Profile | pb.Comment | undefined {
    let it = item.itemType
    if (it.case == field) {
        return it.value
    }
}

And I just discovered that when I set a field, I have to do:

// Nope: item.comment = comment
item.itemType = {case: "comment", value: comment}

It would be nice if generated code would just expose oneof fields as top-level fields (or properties) like other protobuf generators.

All that said, really enjoying protobuf-es so far. Native ESMsupport works so much more nicely than trying to get Google’s protobuf implementation to compile to all my targets. 😄

Issue Analytics

  • State:open
  • Created 9 months ago
  • Comments:8 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
jcreadycommented, Dec 14, 2022

I believe that setup would make it more painful to generically handle the oneof as I don’t thinks there’s a way to use a switch at all. Object.keys(item.oneof)[0] is just string and even if cast to keyof typeof item.oneof that still won’t narrow the value. This means the only way to generically handle a oneof would be via if-statements checking that the property value wasn’t undefined.

So let’s imagine that instead of just Post or Profile we had a dozen different messages that were part of the oneof. And let’s also imagine each message had a common field, a string id, that we wished to return from a function.

// Before
function getItemId(item: Item): string | undefined {
  switch (item.itemType?.case) {
    case "post":
    case "profile":
    //...more cases
    case "whatever":
      return item.itemType.value.id;
  }
}

// After
function getItemId(item: Item): string | undefined {
  if (item.itemType?.post) {
    return item.itemType.post.id;
  }
  if (item.itemType?.profile) {
    return item.itemType.profile.id;
  }
  //...more cases
  if (item.itemType?.whatever) {
    return item.itemType.whatever.id;
  }
}

Another thing to consider is how the number of fields inside the oneof would drastically increase the size of the type in the generated code:

// The number of lines is effectively:
// Before: N * 2
// After:  N ^ 2

// Before
type ItemType = {
  value: Post;
  case: "post";
} | {
  value: Profile;
  case: "profile";
} | {
  //...nine more cases with only two properties: value and case
} | {
  value: Whatever;
  case: "whatever";
} | { case: undefined; value?: undefined }



// After
type ItemType = {
  profile: Profile;
  post?: undefined;
  //...9 more properties which are optionally undefined
  whatever?: undefined;
} | {
  profile?: undefined;
  post: Post;
  //...9 more properties which are optionally undefined
  whatever?: undefined;
} | {
  //...12 properties where the defined property is field 3
} | {
  //...12 properties where the defined property is field 4
} | {
  //...12 properties where the defined property is field 5
} | {
  //...12 properties where the defined property is field 6
} | {
  //...12 properties where the defined property is field 7
} | {
  //...12 properties where the defined property is field 8
} | {
  //...12 properties where the defined property is field 9
} | {
  //...12 properties where the defined property is field 10
} | {
  //...12 properties where the defined property is field 11
} | {
  profile?: undefined;
  post?: undefined;
  //...9 more properties which are optionally undefined
  whatever?: Whatever;
} | {
  profile?: undefined;
  post?: undefined;
  //...9 more properties which are optionally undefined
  whatever?: undefined;
}
0reactions
lwhiteleycommented, Dec 16, 2022

https://gist.github.com/timostamm/d36a6f15b010cbd8b0bf91e734df8cf3

@timostamm Thanks again for the script. worked perfectly but two things i had to do

  1. had to run chmod +x protoc-gen-oneofhelper.ts
  2. had to modify the code a bit due to some conflicts with similar named oneof properties
const titleCase = (value: string) => {
  return value.replace(/^[a-z]/, (v) => v.toUpperCase());
};

// prettier-ignore
function generateMessage(f: GeneratedFile, message: DescMessage) {
  for (const oo of message.oneofs) {
    // Name of the enum we are about to generate
    const name = titleCase(message.name) + '_' + titleCase(localName(oo)) + "Case";
    f.print`export const ${name} = {`;
    for (const field of oo.fields) {
      f.print`  ${field.name.toUpperCase()}: '${localName(field)}',`;
    }
    f.print`} as const;
`;
  }
  for (const nestedMessage of message.nestedMessages) {
    generateMessage(f, nestedMessage);
  }
}

Not the exact migration 1:1 but works fine as I can just do an import as to rename it to the desired name 👍🏾

Read more comments on GitHub >

github_iconTop Results From Across the Web

repeated oneof support? · Issue #2592 · protocolbuffers/protobuf
I have a use case where I need to develop protobuf specs for certain models in the Facebook API. They have one particular...
Read more >
Protobuf Any and Oneof fields for variant types - Microsoft Learn
Learn how to use the Any type and the Oneof keyword to represent variant object types in messages.
Read more >
Protobuf oneof type always nil when Unmarshalling
I'm using kafka to create an event driven system, so when this event is picked up by the kafka consumer, I unmarshal the...
Read more >
Why You Need oneOf in Your OpenAPI Specifications - APIMatic
Using oneOf in response types can help your API describe a variety of schemas, and consequently, the server response will validate against one ......
Read more >
Decoding protobuf message with a oneof field - MongoDB
Is this use-case supported by the mongo-go-driver? If not, is the only solution to this to write my own decoder?
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found