curioswitch / protobuf-jackson

High performance protobuf JSON marshaler based on Jackson.
MIT License
18 stars 4 forks source link

Support for custom WellKnownTypeMarshaller #40

Open pdepietri opened 1 week ago

pdepietri commented 1 week ago

Thanks you for a great project!

I am trying to use the extended Google types, such as google.type.Date in my project and would like the ability to add custom WellKnownTypeMarshaller in order to serialize dates using ISO 8601 date format instead of the individual fields.

Sample proto:

import "google/type/date.proto";
message DateRage {
  google.type.Date startDate = 1;
  google.type.Date endDate = 2;
}

Produces the following JSON output

{
  "startDate": {
    "year": 1,
    "month": 1,
    "day": 1
  },
  "endDate": {
    "year": 9999,
    "month": 12,
    "day": 31
  }
}

The desired output would be

{
  "startDate": "0001-01-01",
  "endDate": "9999-12-31"
}

Adding support for custom marshallers would require making WrapperMarshaller and WellKnownTypeMarshaller and their constructors public and adding a method to MessageMarshaller builder in order to add custom type marshallers.

Sample custom marshaller for google.type.Date:

import java.io.IOException;
import java.time.LocalDate;
import java.util.Locale;
import org.curioswitch.common.protobuf.json.ParseSupport;
import org.curioswitch.common.protobuf.json.WellKnownTypeMarshaller.WrapperMarshaller;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.protobuf.TextFormat.ParseException;
import com.google.type.Date;

public class DateMarshaller extends WrapperMarshaller<Date> {

  public static final DateMarshaller INSTANCE = new DateMarshaller();

  DateMarshaller() {
    super(Date.getDefaultInstance());
  }

  @Override
  public void doMerge(JsonParser parser, int unused, Message.Builder messageBuilder)
      throws IOException {
    Date.Builder builder = (Date.Builder) messageBuilder;
    try {
      String val = ParseSupport.parseString(parser);
      LocalDate localDate = LocalDate.parse(val);
      Date date = Date.newBuilder()
          .setYear(localDate.getYear())
          .setMonth(localDate.getMonthValue())
          .setDay(localDate.getDayOfMonth())
          .build();
      builder.mergeFrom(date);
    } catch (ParseException e) {
      throw new InvalidProtocolBufferException("Failed to readValue date: " + parser.getText(),
          new IOException(e));
    }
  }

  @Override
  public void doWrite(Date message, JsonGenerator gen) throws IOException {
    String format = String.format(Locale.ENGLISH, "%1$04d-%2$02d-%3$02d", message.getYear(),
        message.getMonth(), message.getDay());
    gen.writeString(format);
  }

}

MessageMarshaller builder would look something like this:

MessageMarshaller.builder()//
        .register(Date.getDefaultInstance())
        .register(DateRage.getDefaultInstance())
        .register(AuditInfo.getDefaultInstance())
        .addCustomMarshaller(DateMarshaller.INSTANCE)
        .build();
chokoswitch commented 6 days ago

Hi @pdepietri - we support custom marshallers via jackson-databind integration such as this

https://github.com/curioswitch/protobuf-jackson/blob/main/src/testDatabind/java/org/curioswitch/common/protobuf/json/ObjectMapperTest.java

Will that work for you? Also see https://github.com/curioswitch/protobuf-jackson/issues/12#issuecomment-1543228596 for some background on the goals of this library, notably we don't intend for the core to have additional features compared to upstream (we would be happy to implement them after upstream does). For custom marshalling, we found it to work well as an implementation of jackson-databind's abstraction though.

C0mbatwombat commented 2 days ago

@pdepietri we were able to do this after #12 got merged. We use it like this:


SimpleModule customSerializers = new SimpleModule();
JsonSerializer<YourProto> serializer = new StdSerializer<>(YourProto.class) {
                @Override
                public void serialize(YourProto value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                    gen.writeString(serializeToString(value));
                }
            };
JsonDeserializer<YourProto> deserializer = new StdDeserializer<>(YourProto.class) {
                @Override
                public YourProto deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
                    String value = p.getValueAsString();
                    return deserializeFromString(value);
                }
            };
customSerializers.addDeserializer(YourProto.class, deserializer).addSerializer(serializer);

    private static MessageMarshaller marshaller = MessageMarshaller.builder()
            .omittingInsignificantWhitespace(true)
            .preservingProtoFieldNames(true)
            .build();
    public static ObjectMapper mapper = new ObjectMapper().registerModule(MessageMarshallerModule.of(marshaller))
            .registerModule(customSerializers);

Now you can use the Jackson mapper to (de-)serialize proto to json, and have a custom serialization format for YourProto.