Syntax
A field is defined as name: Type
, where name
is the field's name, and Type
is the field's type. For example:
name: stringimage: Sequence<uint8>address: WellKnownType::Uri
Fields can be separated by either whitespace or a single comma. You would typically use a comma when fields are on the same line, as in:
compact struct Point { x: int32, y: int32 }
The type of a field can be a primitive type, a user-defined type, or a constructed type.
A constructed type is a built-in generic type with arguments for all type parameters. For example, Sequence<string>
and Dictionary<int32, string>
are constructed types.
Optional type
When you define a field, you can specify whether this field must hold a value of its type, or if holding a special "not set" value is also acceptable. In this latter situation, you would give the field an optional type, with a ?
suffix.
This special "not set" value corresponds to null
in C#, std::nullopt
in C++, nil
in Swift, etc.
For example:
A regular field with an optional type, as shown in the example above, is a mandatory field. It just happens that a valid value for this field is "not set". On the other hand, a tagged field (described below) is a truly optional field that the sender or recipient may not know at all.
Tagged fields
A field can have a tag before its name, which makes this field a "tagged field". A tag consists of the tag
keyword followed by a tag number in parenthesis. For example:
Tagged fields allow you to change your Slice definitions while maintaining on-the-wire compatibility with applications that use older or newer Slice definitions without these tagged fields.
A tagged field can have any type, provided this type is marked optional, as shown in the example above.
Tag number
A tag number is a non-negative integer.
The scope of a tag number is the enclosing type. For example, the following definitions are correct, with several tag(1)
in different scopes:
Tag sorting
A tagged field can appear anywhere in the enclosing type's field list, in particular before or after a non-tagged field. You don't need to sort tagged fields by tag number. For instance, the Person
below has a valid though unusual field list:
Tag semantics
A regular (non-tagged) field is mandatory: it's always encoded by the sender, even when its type is optional. Later on, the recipient expects to find one or more bytes for this field in the byte stream. If the sender and recipient don't agree on this field—their Slice definitions are not the same—the decoding will fail.
On the other hand, a tagged field tolerates mismatches. The sender can encode a tagged field that the recipient doesn't know about (it will be ignored), and the recipient can expect a tagged field that the sender doesn't know (the recipient gets a "not set" value in this case).
You can add, remove and reorder tagged fields over time while maintaining on the wire compatibility. The only constraint is you can never change the type associated with a tag number. If the type associated with tag 7 is a string, it must always remain a string; if you were to reuse tag 7 with another type, you would break on the wire compatibility with applications that expect tag 7 fields (in this tag number scope) to be encoded as strings.
C# mapping
A field name: Type
is mapped to a C# property with the same name, with name converted to Pascal case. The type of the C# property is the mapped C# type for Type
.
When a Slice field type maps to a non-nullable C# reference type (for example, a C# string), the mapped property is "required". This ensures you provide a value for this property during construction or initialization of the enclosing type.
Tagged fields are mapped just like regular fields. The tag and tag number don't appear in the mapped C# API.
Optional type in C#
Optional types are mapped to nullable C# types. For example, int32?
is mapped to int?
in C#.