gRPC With. Net Proto Types Created: 23 Mar 2026 Updated: 23 Mar 2026

Protobuf OneOf and Options

1. What is OneOf?

Think of a oneof like a radio button group on a form — you can only select one option at a time. If you pick a new one, the previous selection is automatically cleared.

In Protobuf, oneof means: "this message has several possible fields, but only one of them can hold a value at any given time."

Real-world example: A notification can be sent via Email or SMS or Push — but not all three at once.

2. OneOf Syntax

Here is the basic syntax:

message Notification {
string title = 1;
string body = 2;

oneof delivery_channel {
EmailDelivery email = 3;
SmsDelivery sms = 4;
PushDelivery push = 5;
}
}

message EmailDelivery {
string email_address = 1;
string subject_line = 2;
}

message SmsDelivery {
string phone_number = 1;
string country_code = 2;
}

message PushDelivery {
string device_token = 1;
string platform = 2;
}

Key points:

  1. The oneof block has a name (delivery_channel).
  2. Each field inside gets its own unique field number (3, 4, 5).
  3. Fields outside the oneof block (title, body) work normally.

3. OneOf with Messages

oneof fields can be scalars or message types. Here is an example with geometric shapes:

message Shape {
string color = 1;

oneof geometry {
Circle circle = 2;
Rectangle rectangle = 3;
Triangle triangle = 4;
}
}

message Circle {
double radius = 1;
}

message Rectangle {
double width = 1;
double height = 2;
}

message Triangle {
double base = 1;
double height = 2;
}

A Shape always has a color, but its geometry is exactly one of: Circle, Rectangle, or Triangle.

4. OneOf in C#

Protobuf generates a special enum and a Case property for each oneof block. For the delivery_channel oneof, you get:

// Auto-generated enum
public enum DeliveryChannelOneofCase {
None = 0,
Email = 3,
Sms = 4,
Push = 5
}

// Property on the Notification class
public DeliveryChannelOneofCase DeliveryChannelCase { get; }

You use a switch expression to check which field is set:

var channelUsed = notification.DeliveryChannelCase switch
{
Notification.DeliveryChannelOneofCase.Email => "Email",
Notification.DeliveryChannelOneofCase.Sms => "SMS",
Notification.DeliveryChannelOneofCase.Push => "Push",
_ => "None"
};

Setting a value clears the previous one:

var notification = new Notification();
notification.Email = new EmailDelivery { EmailAddress = "a@b.com" };
// DeliveryChannelCase == Email

notification.Sms = new SmsDelivery { PhoneNumber = "555-1234" };
// DeliveryChannelCase == Sms (Email is now cleared!)

5. OneOf Rules and Restrictions

RuleExplanation
Only one field activeSetting a new field clears the previous one automatically
Cannot use repeatedoneof fields cannot be repeated or map
Default is NoneIf nothing is set, the Case property returns None
Field numbers are sharedOneof fields share the message's field number space — no duplicates allowed
Cannot contain another oneofYou cannot nest a oneof inside another oneof

6. What is Option?

Options are metadata annotations you attach to various proto elements — files, messages, fields, services, or methods. They do not change the data itself; they change how the code is generated or how tools interpret the proto file.

Think of options like HTML attributes — they provide extra information about an element.

7. Common Built-in Options

LevelOptionWhat It Does
Fileoption csharp_namespace = "MyApp";Sets the C# namespace for all generated types in this file
Fileoption java_package = "com.example";Sets the Java package name
Fileoption go_package = "example.com/pb";Sets the Go import path
Fileoption java_multiple_files = true;Generates separate .java files for each message
Field[deprecated = true]Marks a field as deprecated — compiler warns on usage
Field[json_name = "myField"]Overrides the JSON field name used during serialization
Messageoption deprecated = true;Marks an entire message as deprecated

The most commonly used option in .NET projects is csharp_namespace.

8. The deprecated Option

The deprecated option marks a field or message as "no longer recommended." The generated C# code will include an [Obsolete] attribute, and the compiler will show a warning when you use it.

message Vehicle {
string make = 1;
string model = 2;
int32 year = 3;

// This field is deprecated — use registration_id instead
string license_plate = 4 [deprecated = true];

string registration_id = 5;
}

In C#, if you try to use vehicle.LicensePlate, you will see a compiler warning:

Warning CS0612: 'Vehicle.LicensePlate' is obsolete.

This is useful when you want to stop using a field but cannot remove it (because existing clients might still send it).

9. Complete Working Example

Here is the full .proto file combining both concepts:

syntax = "proto3";

option csharp_namespace = "IndepthProtobuf";

package oneof_options;

message Notification {
string title = 1;
string body = 2;

oneof delivery_channel {
EmailDelivery email = 3;
SmsDelivery sms = 4;
PushDelivery push = 5;
}
}

message EmailDelivery {
string email_address = 1;
string subject_line = 2;
}

message SmsDelivery {
string phone_number = 1;
string country_code = 2;
}

message PushDelivery {
string device_token = 1;
string platform = 2;
}

message Shape {
string color = 1;

oneof geometry {
Circle circle = 2;
Rectangle rectangle = 3;
Triangle triangle = 4;
}
}

message Circle { double radius = 1; }
message Rectangle { double width = 1; double height = 2; }
message Triangle { double base = 1; double height = 2; }

message Vehicle {
string make = 1;
string model = 2;
int32 year = 3;
string license_plate = 4 [deprecated = true];
string registration_id = 5;
}

service NotificationService {
rpc SendNotification (SendNotificationRequest) returns (SendNotificationResponse);
rpc CalculateArea (CalculateAreaRequest) returns (CalculateAreaResponse);
}

And the C# service implementation that handles the oneof:

public class NotificationServiceImpl : NotificationService.NotificationServiceBase
{
public override Task<SendNotificationResponse> SendNotification(
SendNotificationRequest request, ServerCallContext context)
{
var notification = request.Notification;

var channelUsed = notification.DeliveryChannelCase switch
{
Notification.DeliveryChannelOneofCase.Email => "Email",
Notification.DeliveryChannelOneofCase.Sms => "SMS",
Notification.DeliveryChannelOneofCase.Push => "Push",
_ => "None"
};

return Task.FromResult(new SendNotificationResponse
{
Success = notification.DeliveryChannelCase != Notification.DeliveryChannelOneofCase.None,
ChannelUsed = channelUsed
});
}

public override Task<CalculateAreaResponse> CalculateArea(
CalculateAreaRequest request, ServerCallContext context)
{
var shape = request.Shape;

var (shapeType, area) = shape.GeometryCase switch
{
Shape.GeometryOneofCase.Circle =>
("Circle", Math.PI * shape.Circle.Radius * shape.Circle.Radius),
Shape.GeometryOneofCase.Rectangle =>
("Rectangle", shape.Rectangle.Width * shape.Rectangle.Height),
Shape.GeometryOneofCase.Triangle =>
("Triangle", 0.5 * shape.Triangle.Base * shape.Triangle.Height),
_ => ("Unknown", 0.0)
};

return Task.FromResult(new CalculateAreaResponse
{
ShapeType = shapeType,
Area = area
});
}
}

10. Common Mistakes

MistakeWhy It's WrongFix
oneof channel { repeated string emails = 1; }repeated is not allowed inside oneofWrap in a message: message EmailList { repeated string emails = 1; }
Not checking the Case propertyAccessing a field that is not set returns default values, leading to bugsAlways check DeliveryChannelCase before accessing the field
Using option keyword for field optionsField options use square brackets, not the option keywordUse string name = 1 [deprecated = true];
Forgetting option is file-level when outside a messageoption csharp_namespace applies to the whole filePlace file-level options near the top of the file

11. Summary

FeaturePurposeC# Mapping
oneofOnly one field can be set at a timeGenerates an enum + Case property; switch to check which field is active
option (file-level)Metadata for the whole file (namespace, package, etc.)csharp_namespace sets the C# namespace
[deprecated = true]Marks a field as obsoleteGenerates [Obsolete] attribute — compiler warning on use
[json_name = "..."]Overrides JSON field nameChanges the JSON property name during serialization


Share this lesson: