Optional Fields

A common issue with Protobufs is that the way that nil values are represented: a zero-valued primitive field isn’t encoded into the binary representation, this means that applications cannot distinguish between zero and not-set for primitive fields.

To support this, the Protobuf project supports some Well-Known types called “wrapper types”. For example, the wrapper type for a bool, is called google.protobuf.BoolValue and is defined as:

  1. // Wrapper message for `bool`.
  2. //
  3. // The JSON representation for `BoolValue` is JSON `true` and `false`.
  4. message BoolValue {
  5. // The bool value.
  6. bool value = 1;
  7. }

When entproto generates a Protobuf message definition, it uses these wrapper types to represent “Optional” ent fields.

Let’s see this in action, modifying our ent schema to include an optional field:

  1. // Fields of the User.
  2. func (User) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.String("name").
  5. Unique().
  6. Annotations(
  7. entproto.Field(2),
  8. ),
  9. field.String("email_address").
  10. Unique().
  11. Annotations(
  12. entproto.Field(3),
  13. ),
  14. field.String("alias").
  15. Optional().
  16. Annotations(entproto.Field(4)),
  17. }
  18. }

Re-running go generate ./..., observe that our Protobuf definition for User now looks like:

  1. message User {
  2. int32 id = 1;
  3. string name = 2;
  4. string email_address = 3;
  5. google.protobuf.StringValue alias = 4; // <-- this is new
  6. }

The generated service implementation also utilize this field. Observe in entpb_user_service.go:

  1. // Create implements UserServiceServer.Create
  2. func (svc *UserService) Create(ctx context.Context, req *CreateUserRequest) (*User, error) {
  3. user := req.GetUser()
  4. m := svc.client.User.Create()
  5. if user.GetAlias() != nil {
  6. m.SetAlias(user.GetAlias().GetValue())
  7. }
  8. m.SetEmailAddress(user.GetEmailAddress())
  9. m.SetName(user.GetName())
  10. res, err := m.Save(ctx)
  11. switch {
  12. case err == nil:
  13. return toProtoUser(res), nil
  14. case sqlgraph.IsUniqueConstraintError(err):
  15. return nil, status.Errorf(codes.AlreadyExists, "already exists: %s", err)
  16. case ent.IsConstraintError(err):
  17. return nil, status.Errorf(codes.InvalidArgument, "invalid argument: %s", err)
  18. default:
  19. return nil, status.Errorf(codes.Internal, "internal: %s", err)
  20. }
  21. }

To use the wrapper types in our client code, we can use helper methods supplied by the wrapperspb package to easily build instances of these types. For example in cmd/client/main.go:

  1. func randomUser() *entpb.User {
  2. return &entpb.User{
  3. Name: fmt.Sprintf("user_%d", rand.Int()),
  4. EmailAddress: fmt.Sprintf("user_%d@example.com", rand.Int()),
  5. Alias: wrapperspb.String("John Doe"),
  6. }
  7. }