Learnitweb

Polymorphic Data Modeling in Protobuf with oneof

In Java, we commonly use interfaces and abstract classes to achieve polymorphism. A base type defines a contract, and multiple implementations provide different behaviors. For example, a Car interface might have a drive() method, and classes like SportsCar and LuxuryCar implement it differently. At runtime, the behavior depends on the concrete type passed.

Protocol Buffers approach this idea differently because Protobuf is about data modeling, not behavior. There are no methods to override or behaviors to customize. Instead, Protobuf provides a mechanism to model polymorphic data — and that mechanism is called oneof.

Conceptual Understanding of oneof

oneof allows you to define a field that can hold exactly one of several possible message types.

You can think of it like:

  • A union type
  • A tagged variant
  • Or a data-oriented version of polymorphism

The key guarantee is:

Only one of the fields inside a oneof can be set at any time.

This is not about behavior like Java polymorphism; it is about representing data that can exist in multiple alternative forms.

Real-World Motivation

Consider login credentials. A user might log in using:

  • Email + password
  • Phone number + OTP

Both represent “credentials,” but their structures differ. Instead of forcing a single structure with many optional fields, oneof lets you model mutually exclusive data clearly.

Another example is server responses:

  • Success response
  • Error response
  • Validation failure
  • Authorization failure

You know you will receive a response, but its form varies. oneof models this cleanly.

Defining Messages

First, define the possible data shapes.

Email Credentials

message Email {
  string address = 1;
  string password = 2;
}

Phone Credentials

message Phone {
  string number = 1;
  string code = 2;
}

Each message can have any number of fields. They do not need to match each other structurally.

Defining the oneof

Now define a wrapper message that uses oneof.

message Credentials {
  oneof login_method {
    Email email = 1;
    Phone phone = 2;
  }
}

This says:

  • A Credentials object contains one login method
  • That method is either email or phone
  • Never both at the same time

The name login_method is just a label for the group. You can choose any meaningful name.

Generated Code Behavior

Even with oneof, Protobuf still generates a normal final class. It does not become an interface or abstract class. However, Protobuf automatically generates:

  • A case enum
  • Helper methods to detect which field is set

For example:

credentials.getLoginMethodCase();

This returns an enum like:

  • EMAIL
  • PHONE
  • LOGINMETHOD_NOT_SET

This enum is your runtime indicator of which variant is present.

Handling oneof in Java

A common pattern is a switch statement.

switch (credentials.getLoginMethodCase()) {
  case EMAIL -> {
    Email email = credentials.getEmail();
    // process email
  }
  case PHONE -> {
    Phone phone = credentials.getPhone();
    // process phone
  }
  case LOGINMETHOD_NOT_SET -> {
    // handle missing data
  }
}

This gives type-safe branching based on actual data.

Creating Instances

Email Credentials

Email email = Email.newBuilder()
    .setAddress("sam@gmail.com")
    .setPassword("admin")
    .build();

Credentials cred = Credentials.newBuilder()
    .setEmail(email)
    .build();

Phone Credentials

Phone phone = Phone.newBuilder()
    .setNumber("123456789")
    .setCode("123")
    .build();

Credentials cred = Credentials.newBuilder()
    .setPhone(phone)
    .build();

You must wrap them inside Credentials. You cannot pass Email where Credentials is expected.

What If You Set Multiple Fields?

This is an important rule.

If you do:

Credentials.newBuilder()
    .setEmail(email)
    .setPhone(phone)
    .build();

You might expect an error, but Protobuf allows it.

However:

The last field set wins.

So in this case:

  • phone overwrites email
  • Only phone remains set

If you reverse the order, email wins.

This overwrite behavior is by design and ensures the oneof guarantee remains intact.

Why oneof Is Powerful

oneof provides several advantages:

  • Clear modeling of mutually exclusive data
  • Smaller serialized size
  • Strong data validation by schema
  • Cleaner API contracts
  • Better documentation of intent

Instead of many optional fields with unclear rules, oneof makes exclusivity explicit.

Complete Working Program

Proto file

syntax = "proto3";

option java_package = "com.example.protobuf";
option java_multiple_files = true;

package demo;

// Email credential type
message Email {
  string address = 1;
  string password = 2;
}

// Phone credential type
message Phone {
  string number = 1;
  string code = 2;
}

// Wrapper with oneof
message Credentials {
  oneof login_method {
    Email email = 1;
    Phone phone = 2;
  }
}

Java Program

package org.learnitweb;


import com.example.protobuf.Credentials;
import com.example.protobuf.Email;
import com.example.protobuf.Phone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {

        // Build Email
        Email email = Email.newBuilder()
                .setAddress("sam@gmail.com")
                .setPassword("admin")
                .build();

        // Build Phone
        Phone phone = Phone.newBuilder()
                .setNumber("1234567890")
                .setCode("9999")
                .build();

        // Credentials with Email
        Credentials emailCred = Credentials.newBuilder()
                .setEmail(email)
                .build();

        // Credentials with Phone
        Credentials phoneCred = Credentials.newBuilder()
                .setPhone(phone)
                .build();

        // Test login handling
        login(emailCred);
        login(phoneCred);

        // Demonstrate "last one wins"
        Credentials both = Credentials.newBuilder()
                .setEmail(email)
                .setPhone(phone) // overwrites email
                .build();

        login(both);
    }

    private static void login(Credentials cred) {

        switch (cred.getLoginMethodCase()) {

            case EMAIL -> {
                System.out.println("Email login:");
                System.out.println("Address: " + cred.getEmail().getAddress());
            }

            case PHONE -> {
                System.out.println("Phone login:");
                System.out.println("Number: " + cred.getPhone().getNumber());
            }

            case LOGINMETHOD_NOT_SET -> {
                System.out.println("No credentials provided.");
            }
        }

        System.out.println("----");
    }
}

Output

Email login:
Address: sam@gmail.com
----
Phone login:
Number: 1234567890
----
Phone login:
Number: 1234567890
----