Customization
Customization is sometimes needed when the generated Protobuf schema does not match what you want. There are several ways to customize the generated schema:
- Codec transformations allow using a different Scala type to generate the Protobuf type, as long as you provide functions to transform from and to the original type.
- Modifiers allow changing the generated schema for common use cases such as renames, comments, reserved indexes, etc.
- Derivation flags allow configuring the derivation process.
Codec transformations
Let's imagine you have the following case class:
case class Order(items: Map[String, Int])It will generate the following Protobuf schema:
message Order {
map<string, int32> items = 1;
}But you want to generate a different Protobuf schema, for example:
message Order {
repeated Item items = 1;
}
message Item {
string id = 1;
int32 count = 2;
}You could change your case class to match this, but you might also want to keep the original type for other purposes. In that case, you can create separate Scala types that match the expected Protobuf schema:
object Proto {
case class Item(id: String, count: Int)
case class Order(items: List[Item])
}Then you can create a codec for the original type by calling transform on the codec for the Protobuf type and providing functions to transform from and to the original type.
val codec: ProtobufCodec[Order] =
ProtobufCodec
.derived[Proto.Order](using ProtobufDeriver)
.transform[Order](
proto => Order(proto.items.map(i => i.id -> i.count).toMap),
order => Proto.Order(order.items.map((id, count) => Proto.Item(id, count)).toList)
)Let's check the rendered schema:
println(codec.render())
// syntax = "proto3";
//
// message Order {
// repeated Item items = 1;
// }
//
// message Item {
// string id = 1;
// int32 count = 2;
// }But what if your type is used by other types? How do you tell Proteus to use the correct codec? This is done by calling .instance on the deriver and providing the codec you want to use. You need an instance of Schema for the type to apply the instance override to it.
val deriver = ProtobufDeriver.instance(codec)Now everywhere you use deriver (rather than the default ProtobufDeriver), your custom codec will be used in place of the default one.
Modifiers
Creating a custom codec is useful, but it is boilerplate, and we want to avoid it when possible. For that reason, Proteus provides modifiers that allow you to easily change the generated schema for common use cases without having to touch the Scala types.
Let's see a simple example. We have this case class:
case class Person(name: String, age: Int)But we want it to be named "User" in the Protobuf schema. There is no need to create a case class User for such a simple use case. Instead, you can apply a modifier to the deriver like this:
import proteus.Modifiers.*
val deriver = ProtobufDeriver.modifier[Person](rename("User"))
println(ProtobufCodec.derived[Person](using deriver).render())
// syntax = "proto3";
//
// message User {
// string name = 1;
// int32 age = 2;
// }If you wanted to rename the name field instead, you could do:
import proteus.Modifiers.*
val deriver = ProtobufDeriver.modifier[Person]("name", rename("full_name"))
println(ProtobufCodec.derived[Person](using deriver).render())
// syntax = "proto3";
//
// message Person {
// string full_name = 1;
// int32 age = 2;
// }Here are the different types of modifiers you can apply:
excluded: Excludes a field or an enum member from the protobuf type.nested: Nests a type inside its parent message instead of creating it at the root level.unnested: Forces a type to be created at the root level and prevent it from being nested by theOneOfFlag.Nestedflag (see below).oneOf: Forces a type to be encoded as aoneofrather than an enum.oneOf(flags: OneOfFlag*): Controls howoneoftypes are encoded—see below for more details.enumPrefix(prefix: String): Prefixes enum members with a string.enumSuffix(suffix: String): Suffixes enum members with a string.comment(comment: String): Adds a comment to a type, field, or enum member.rename(name: String): Renames a type, field, or enum member.reserved(indexes: Int*): Adds some reserved indexes to a type. Those reserved indexes will be skipped when deriving the protobuf type. If this modifier is applied to a field, the field will use the given index(es).deprecated: Marks a field or an enum member as deprecated. It will be rendered with the[deprecated = true]option in the protobuf definition.
There are two possible OneOfFlag values:
Inline: Inlines theoneofas a single field inside the parent message.
message Example {
int32 field = 1;
oneof address { // inline oneof field
Email email = 2;
Phone phone = 3;
}
}Nested: Makes theoneofa separate type nested inside the parent message.
message Example {
message Address { // nested oneof type
oneof value {
Email email = 1;
Phone phone = 2;
}
}
int32 field = 1;
Address address = 2;
}Derivation flags
Sometimes you might want some specific behavior to apply to all types during derivation, without having to create a custom codec or modifier for each type. For that, Proteus provides derivation flags that allow configuring the derivation process.
import proteus.ProtobufDeriver.DerivationFlag
val deriver = ProtobufDeriver.enable(DerivationFlag.AutoPrefixEnums)Here are the different types of derivation flags you can apply:
OptionalAsOneOf: Instead of using theoptionalkeyword, optional fields will be encoded as aoneoffield with two cases: one of typeEmpty(an empty message) and one for the actual value.AutoPrefixEnums: Automatically prefix the enum members with the type name.AutoSuffixEnums: Automatically suffix the enum members with the type name.NestedOneOf: All types used inoneoffields will be encoded as nested types inside the parent message. This is equivalent to applying theoneOf(Nested)modifier to all types used inoneoffields.
Let's check a quick example.
enum Status { case Active, Inactive, Pending }
val deriver = ProtobufDeriver.enable(DerivationFlag.AutoPrefixEnums)
println(ProtobufCodec.derived[Status](using deriver).render())
// syntax = "proto3";
//
// enum Status {
// STATUS_ACTIVE = 0;
// STATUS_INACTIVE = 1;
// STATUS_PENDING = 2;
// }As you can see, the enum members are prefixed with "STATUS_".