Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.hubspot.jackson.datatype.protobuf;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.google.common.base.Preconditions;
import com.google.protobuf.ProtocolMessageEnum;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

public final class ProtobufEnumAsNumber {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to reorganize this into two separate classes if you prefer that style.


private ProtobufEnumAsNumber() {}

public static class Serializer extends StdSerializer<ProtocolMessageEnum> {

protected Serializer() {
super(ProtocolMessageEnum.class);
}

@Override
public void serialize(ProtocolMessageEnum protocolMessageEnum, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
jsonGenerator.writeNumber(protocolMessageEnum.getNumber());
}
}

@SuppressFBWarnings({"SE_TRANSIENT_FIELD_NOT_RESTORED", "SE_NO_SERIALVERSIONID"})
public static class Deserializer extends StdDeserializer<ProtocolMessageEnum> implements ContextualDeserializer {

private final transient Class<?> enumClass;
private final transient Method forNumberMethod;

protected Deserializer() {
this(null, null);
}

protected Deserializer(Class<?> enumClass, Method forNumberMethod) {
super(ProtocolMessageEnum.class);
this.enumClass = enumClass;
this.forNumberMethod = forNumberMethod;
}

@Override
public JsonDeserializer<?> createContextual(DeserializationContext context, BeanProperty beanProperty) throws JsonMappingException {
// beanProperty is null when the type to deserialize is the top-level type or a generic type, not a type of a bean property
JavaType javaType = context.getContextualType();

if (javaType == null) {
javaType = beanProperty.getMember().getType();
}

Class<?> enumClass = javaType.getRawClass();

if (enumClass == this.enumClass
&& this.forNumberMethod != null) {
// short circuit if this instance is already correctly configured
return this;
}

Method forNumberMethod;

try {
forNumberMethod = enumClass.getMethod("forNumber", int.class);
} catch (NoSuchMethodException e) {
context.reportBadDefinition(javaType, "Could not find a static forNumber(int) method on this type");
// the previous method should have thrown
throw new AssertionError();
}

return new Deserializer(enumClass, forNumberMethod);
}

@Override
public ProtocolMessageEnum deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
int intValue = jsonParser.getIntValue();
// Class<?> enumClass = Preconditions.checkNotNull(javaType, "javaType").getRawClass();
try {
return (ProtocolMessageEnum) Preconditions.checkNotNull(forNumberMethod, "forNumberMethod").invoke(null, intValue);
} catch (IllegalAccessException | InvocationTargetException e) {
context.reportWrongTokenException(ProtocolMessageEnum.class, JsonToken.VALUE_NUMBER_INT, wrongTokenMessage(context));
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I borrowed this pattern from the other deserializer implementations, but I'm not sure if this is really a wrong-token exception.

// the previous method should have thrown
throw new AssertionError();
}
}
}

// TODO share this?
private static String wrongTokenMessage(DeserializationContext context) {
return "Can not deserialize instance of com.google.protobuf.ProtocolMessageEnum out of " + context.getParser().currentToken() + " token";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.hubspot.jackson.datatype.protobuf;

import static com.hubspot.jackson.datatype.protobuf.util.ObjectMapperHelper.camelCase;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;

import org.junit.Test;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.hubspot.jackson.datatype.protobuf.util.TestProtobuf;
import com.hubspot.jackson.datatype.protobuf.util.TestProtobuf.Enum;

public class ProtobufEnumAsNumberTest {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to add more test cases if you think there are major blind spots.


@Test
public void itSerializesToEnumNumber() {
ObjectMapper mapper = camelCase();

for (Enum anEnum : Enum.values()) {
ObjectWithProtobufEnumField input = new ObjectWithProtobufEnumField();
input.anEnum = anEnum;

JsonNode node = mapper.valueToTree(input);

assertThat(node.has("anEnum")).isTrue();
assertThat(node.get("anEnum").isInt()).isTrue();
assertThat(node.get("anEnum").intValue()).isEqualTo(anEnum.getNumber());
}
}

@Test
public void itDeserializesFromEnumNumber() throws JsonProcessingException {
ObjectMapper mapper = camelCase();

for (Enum anEnum : Enum.values()) {
ObjectWithProtobufEnumField input = new ObjectWithProtobufEnumField();
input.anEnum = anEnum;

JsonNode node = mapper.valueToTree(input);

ObjectWithProtobufEnumField output = mapper.treeToValue(node, ObjectWithProtobufEnumField.class);
assertThat(output).isNotSameAs(input);
assertThat(output.anEnum).isEqualTo(input.anEnum);
}
}

@Test
public void itFailsWhenSerializingNonProtobufEnumFields() {
ObjectMapper mapper = camelCase();

for (NonProtobufEnum anEnum : NonProtobufEnum.values()) {
ObjectWithVanillaEnumField input = new ObjectWithVanillaEnumField();
input.anEnum = anEnum;

try {
mapper.valueToTree(input);
fail("expected an exception to be thrown");
} catch (Exception e) {
assertThat(e).isInstanceOf(IllegalArgumentException.class);
}
}
}

@Test
public void itFailsWhenDeserializingNonProtobufEnumFields() {
ObjectMapper mapper = camelCase();

for (NonProtobufEnum anEnum : NonProtobufEnum.values()) {
ObjectWithProtobufEnumField input = new ObjectWithProtobufEnumField();
input.anEnum = Enum.valueOf(anEnum.name());

JsonNode node = mapper.valueToTree(input);

try {
mapper.treeToValue(node, ObjectWithVanillaEnumField.class);
fail("expected an exception to be thrown");
} catch (Exception e) {
assertThat(e).isInstanceOf(JsonMappingException.class);
}
}
}

private static class ObjectWithProtobufEnumField {

@JsonSerialize(using = ProtobufEnumAsNumber.Serializer.class)
@JsonDeserialize(using = ProtobufEnumAsNumber.Deserializer.class)
private TestProtobuf.Enum anEnum;
}

private static class ObjectWithVanillaEnumField {

@JsonSerialize(using = ProtobufEnumAsNumber.Serializer.class)
@JsonDeserialize(using = ProtobufEnumAsNumber.Deserializer.class)
private NonProtobufEnum anEnum;
}

private enum NonProtobufEnum {
ONE, TWO
}
}