Protocol Buffers Nested Messages
1. What Are Nested Messages?
In Protocol Buffers, a nested message is a message type defined inside another message type. It works just like a regular message, but it is scoped to its parent.
Real-world analogy: Think of a filing cabinet. The cabinet itself is the parent message. Inside it, you have folders — those are nested messages. A folder labeled "Invoices" only makes sense inside that cabinet. You could have another cabinet with its own "Invoices" folder, and they wouldn't conflict.
Here is the simplest example:
In this example, Address is a nested message inside Customer. It belongs to Customer and is accessed as Customer.Address from outside.
2. Why Nest Messages?
You might wonder: why not just define every message at the top level? There are several good reasons to nest:
| Reason | Explanation | Example |
|---|---|---|
| Organization | Groups related data together, making your .proto file easier to read | PurchaseOrder.OrderItem clearly belongs to PurchaseOrder |
| Name scoping | Avoids name collisions — two parent messages can each have their own Address | Customer.Address vs Company.Address |
| Intent | Signals to other developers that this type is only meaningful in the context of its parent | PurchaseOrder.PaymentInfo is clearly about an order's payment |
| Encapsulation | Keeps helper types close to where they are used | A Dimensions message inside ProductListing |
Think of it this way: If someone asks "What is an Address?", the answer depends on context. A customer's address and a warehouse's address might have completely different fields. Nesting makes the context explicit.
3. Your First Nested Message
Let's build a Customer message that has an address. Instead of defining Address at the top level, we'll nest it inside Customer:
Let's break down what's happening:
| Part | What It Does |
|---|---|
message Customer { ... } | Defines the parent message |
message Address { ... } (inside Customer) | Defines a nested message — only exists within Customer's scope |
Address billing_address = 4; | Uses the nested message as a field type (no prefix needed inside the parent) |
Address shipping_address = 5; | Same nested type reused for another field — a customer can have two addresses |
Notice that inside Customer, you refer to the nested type simply as Address. You do NOT need to write Customer.Address when referencing it from within the same parent.
4. How Nested Messages Map to C#
When protobuf generates C# code, nested messages become inner classes under a special Types class inside the parent:
Proto definition:
Generated C# structure (simplified):
How you use it in C#:
Key C# pattern: Nested messages are always accessed viaParentMessage.Types.NestedMessage. TheTypesclass is auto-generated by protobuf to hold all nested types.
This is one of the most important things to remember:
| In Proto | In C# |
|---|---|
Customer.Address | Customer.Types.Address |
PurchaseOrder.OrderItem | PurchaseOrder.Types.OrderItem |
ProductListing.Dimensions | ProductListing.Types.Dimensions |
5. Reusing Nested Messages from Outside
A nested message can be referenced from any other message using the Parent.NestedType syntax:
In C#, this looks like:
Important: Even though the nested message is defined inside Customer, it can be used anywhere. Nesting is about organization and naming, not about restricting access.6. Deeply Nested Messages (Multiple Levels)
You can nest messages inside nested messages — as many levels deep as you need:
Here we have three levels: University > Department > Course.
In C#:
Referencing deeply nested types from other messages:
Warning: While deep nesting is possible, going more than 2-3 levels deep makes your code harder to work with — especially in C# where each level adds .Types. to the path. Keep it practical.7. Combining Nested Messages and Enums
A parent message can contain both nested messages and nested enums. This is a powerful pattern for building self-contained data structures:
In C#:
Notice the pattern:
- Nested enum:
ProductListing.Types.Category - Nested message:
ProductListing.Types.Dimensions
Both live under the Types class in C#.
8. Repeated Nested Messages (Lists)
One of the most common patterns is a parent message that contains a list of nested messages:
The repeated keyword means "a list of." So repeated OrderItem items means "this order has zero or more items."
In C#:
Key point: In C#,repeatedfields becomeRepeatedField<T>— a special list-like collection. You use.Add(),.Count, andforeachjust like with any other list. You do not assign a new list — you add items to the existing one.
9. Complete Working Example — Order Management Service
Let's put everything together in a complete, working .proto file that uses nested messages in a gRPC service:
File: Protos/nested.proto
This service has two RPCs:
- CreateOrder — Receives a customer, a list of items, and payment info. Returns the new order ID and total.
- GetOrder — Receives an order ID. Returns the full order with all nested data.
10. The C# Service Implementation
Here is the complete C# service that implements the OrderManagement gRPC service:
File: Services/OrderManagementService.cs
Let's look at how nested messages are used in this service:
| Code | What It Does |
|---|---|
request.Customer | Accesses the Customer message (which itself contains nested Address messages) |
request.Items | Accesses the repeated list of PurchaseOrder.OrderItem nested messages |
request.Payment | Accesses the PurchaseOrder.PaymentInfo nested message |
order.Items.Add(item) | Adds a nested OrderItem to the repeated field |
order.Payment.TotalAmount | Reads/writes a field on a nested message |
Don't forget to register the service in Program.cs:
11. When to Nest vs. Keep Top-Level
This is one of the most important design decisions when writing proto files. Here is a simple decision guide:
| Scenario | Decision | Why |
|---|---|---|
| Only one message uses this type | Nest it | Keeps related things together; signals intent |
| Multiple messages share the same type | Top-level | Avoids confusing ownership; easier to reference |
| The type is a general concept (Address, Money, Timestamp) | Top-level | General concepts belong in a shared space |
| The type only makes sense within its parent's context | Nest it | PurchaseOrder.OrderItem clearly belongs to a PurchaseOrder |
| The nested type name would conflict with others | Nest it | Scoping prevents name collisions |
You need the type in a different .proto file | Top-level | Easier to import and reference |
Rule of thumb: If you can describe the type without mentioning its parent (e.g., "an address," "a timestamp"), it should probably be top-level. If it needs its parent for context (e.g., "an order's line item"), nest it.
12. Common Mistakes to Avoid
Mistake 1: Forgetting .Types. in C#
| ✘ WRONG |
|---|
| ✔ CORRECT |
|---|
Mistake 2: Unnecessary nesting when the type is shared
| ✘ BAD — Address used by many types |
|---|
| ✔ BETTER — Top-level shared type |
|---|
Mistake 3: Nesting too many levels deep
| ✘ TOO DEEP — Hard to use in C# |
|---|
| ✔ PRACTICAL — 1-2 levels max |
|---|
Mistake 4: Trying to assign a new list to a repeated field
| ✘ WRONG — Cannot assign to repeated field |
|---|
| ✔ CORRECT |
|---|
13. Summary
Here is everything you learned in this guide:
| Concept | Key Point |
|---|---|
| Nested message | A message defined inside another message, scoped to its parent |
| Proto syntax | Just write message Inner { ... } inside the outer message block |
| C# access pattern | Parent.Types.NestedType — always include .Types. |
| Referencing from outside | Use Parent.NestedType in proto; Parent.Types.NestedType in C# |
| Deep nesting | Possible but avoid more than 2-3 levels for readability |
| With enums | Nested enums and messages work the same way under .Types. |
| Repeated nested | Use repeated NestedType field_name for lists; use .Add() in C# |
| When to nest | When the type only makes sense within its parent's context |
| When to use top-level | When the type is shared across multiple messages or is a general concept |
Final tip: Nested messages are one of protobuf's most useful features for building clean, well-organized APIs. They let you group related data, prevent naming conflicts, and clearly communicate the structure of your gRPC service. Start with 1-2 levels of nesting and keep things simple!