Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Determine enum values by serializing in Jackson module #137

Closed
ldaley opened this issue Sep 15, 2020 · 5 comments
Closed

Determine enum values by serializing in Jackson module #137

ldaley opened this issue Sep 15, 2020 · 5 comments
Labels
question Further information is requested

Comments

@ldaley
Copy link

ldaley commented Sep 15, 2020

If using a custom global enum serializer registered with an object mapper, the inferred values in the schema are incorrect. Instead of reverse engineering how Jackson will serialize the value, it might be better to just serialize through Jackson after allowing the user to register whatever modules/serializers they need with the mapper used for this serialization.

@CarstenWickner
Copy link
Member

CarstenWickner commented Sep 15, 2020

Hi Luke,

Thank you for your input.

You’re actually not the first to suggest making use of the already configured custom serializers.
However, I still struggle to wrap my head around how this is supposed to work in a generic way.

What Jackson does, is producing (part of) a JSON instance – not a schema. How do you propose to produce all possible outputs and derive a common schema from that?

  • Since you’re talking about an enum, we could iterate over all its supported values easily.
  • If those are then plain strings still, you could collect them into a JSON schema ”enum” – no problem there.
  • If the Jackson ObjectMapper produces anything but atomic it values, you’d still have to convert and combine those in a schema. That’s where the challenge is.

If you could provide your custom serializer and/or examples of an enum and its expected representation as a schema, we could come up with something. Currently, I’m assuming it’ll need to be a CustomDefinitionProvider to replace the few standard options for deriving schemas from enums.

Cheers,
Carsten

@ldaley
Copy link
Author

ldaley commented Sep 15, 2020

Hi Carsten,

Here's the module I am using to control how enums are serialized. It is using Guava's CaseFormat.

public final class EnumCaseConversionJacksonModule extends SimpleModule {

    private final CaseFormat deserialized;
    private final CaseFormat serialized;

    public EnumCaseConversionJacksonModule(CaseFormat serialized) {
        this(CaseFormat.UPPER_UNDERSCORE, serialized);
    }

    public EnumCaseConversionJacksonModule(CaseFormat deserialized, CaseFormat serialized) {
        super("enum case conversion");
        this.deserialized = deserialized;
        this.serialized = serialized;
    }

    {
        setDeserializerModifier(new BeanDeserializerModifier() {
            @Override
            public JsonDeserializer<Enum> modifyEnumDeserializer(
                DeserializationConfig config,
                JavaType type,
                BeanDescription beanDesc,
                JsonDeserializer deserializer
            ) {
                // Only use custom deserializer if @JsonCreator isn't specified
                @SuppressWarnings("ConstantConditions")
                boolean hasCreator = Iterables.any(beanDesc.getFactoryMethods(), m -> m.hasAnnotation(JsonCreator.class));
                if (hasCreator) {
                    @SuppressWarnings("unchecked")
                    JsonDeserializer<Enum> enumJsonDeserializer = (JsonDeserializer<Enum>) super.modifyEnumDeserializer(config, type, beanDesc, deserializer);
                    return enumJsonDeserializer;
                } else {
                    return new JsonDeserializer<Enum>() {
                        @Override
                        public Enum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
                            String value = p.getValueAsString();
                            if (StringUtils.isBlank(value)) {
                                return null;
                            } else {
                                return toEnum(type.getRawClass(), value);
                            }
                        }

                        private <T extends Enum<T>> Enum<T> toEnum(Class<?> rawClass, String value) {
                            Class<T> cast = Types.cast(rawClass);
                            return Enum.valueOf(cast, serialized.to(deserialized, value));
                        }
                    };
                }
            }
        });

        setSerializerModifier(new BeanSerializerModifier() {
            @Override
            public JsonSerializer<?> modifyEnumSerializer(SerializationConfig config, JavaType valueType, BeanDescription beanDesc, JsonSerializer<?> serializer) {
                // Only use custom serializer if @JsonValue isn't specified
                if (beanDesc.findJsonValueAccessor() == null) {
                    return new StdSerializer<Enum>(Enum.class) {
                        @Override
                        public void serialize(Enum value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
                            jgen.writeString(deserialized.to(serialized, value.name()));
                        }
                    };
                }

                return super.modifyEnumSerializer(config, valueType, beanDesc, serializer);
            }
        });
    }
}

How do you propose to produce all possible outputs and derive a schema from that?

I think enums are a bit of a special case here and am only suggesting to use this strategy for that. I don't see it being feasible for arbitrary types.

My use case is that I want conventional Java enums, but lower-camel symbols in the data file. And, I want to achieve this without having to markup all of my enum classes.

I'm very interested in any pointers on how to achieve the same with a custom definition provider.

@ldaley
Copy link
Author

ldaley commented Sep 15, 2020

FYI FasterXML/jackson-databind#2667 is relevant here.

@CarstenWickner
Copy link
Member

CarstenWickner commented Sep 15, 2020

Hi Luke,

Once that feature is introduced in Jackson, I'm happy to add general support for it.
Until then, I suggest you simply re-purpose the existing EnumModule class, which was designed to allow exactly this kind of alternative to-string logic:

ObjectMapper objectMapper = new ObjectMapper()
        .registerModule(new EnumCaseConversionJacksonModule(CaseFormat.LOWER_CAMEL));
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON);
configBuilder.with(new EnumModule(possibleEnumValue -> {
    try {
        String valueInQuotes = objectMapper.writeValueAsString(possibleEnumValue);
        return valueInQuotes.substring(1, valueInQuotes.length() - 1);
    } catch (JsonProcessingException ex) {
        return null;
    }
}));
SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
JsonNode jsonSchema = generator.generateSchema(TestEnum.class);

For this example:

enum TestEnum {
    FIRST_VALUE, SECOND_VALUE;
}

The produced schema looks like this:

{
  "$schema" : "https://json-schema.org/draft/2019-09/schema",
  "type" : "string",
  "enum" : [ "firstValue", "secondValue" ]
}

@CarstenWickner
Copy link
Member

CarstenWickner commented Sep 15, 2020

I guess I could add that as a standard Option as well.
But since it's a simple enough configuration, I'd prefer to wait and see what happens on the Jackson side for now.

The need for an ObjectMapper makes this a bit awkward. Sure, there is an ObjectMapper instance available in the generator, but you might want a different ObjectMapper for the schema generation vs. the handling of enums: The above example demonstrates that: a decoupled ObjectMapper just used for the enum value serialization.
You might want to change the handling of the JsonProcessingException to be stricter but otherwise, this should suit your use-case fine.

@CarstenWickner CarstenWickner added the question Further information is requested label Sep 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Development

No branches or pull requests

2 participants