bryce bryce - 7 months ago 36
Java Question

Gson SerializedName change in some environnement

I have a DTO with some GSon annotation.

My probleme is that the value of these annotations have to change if my application run in developpement or in staging or in production...


For the moment, I have to package my application with the different value and I want this to be automatic... It is in a Spring Boot application and I want to use the

spring.profiles.active
to tell my application to take the right
serializedName



Here is the kind of code I use


// Tests
// @SerializedName("customfield_10123")
// Prod
@SerializedName("customfield_10114")
private ActionDto action;


I hope there is a better way to do it?

Answer

Here is a very crude example on how you can achieve what you want:

First create a propery file for each possible profile (name can be anything, but the profile must be on the name):

application-dev.properties
application-prod.properties
...

Populate the properties with the values you want for each key accordingly to each profile:

test=abc.test
...

Annotate your POJOs:

public class Foo {

    @SerializedName("${test}")
    private String name;

    ...

}

Create a custom serializer for your class, which will interpret the custom names, something like this:

public class FooSerializer implements JsonSerializer<Foo> {

    private static final Pattern PATTERN = Pattern.compile("\\$\\{(.*)\\}");
    private static Properties props;

    static {
        try {
            Resource resource = new ClassPathResource(String.format("/application-%s.properties", System.getProperty("spring.profiles.active")));
            props = PropertiesLoaderUtils.loadProperties(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public JsonElement serialize(Foo foo, Type type, JsonSerializationContext jsonSerializationContext) {

        Field[] fields = foo.getClass().getDeclaredFields();

        JsonObject object = new JsonObject();

        for (Field field : fields) {
            field.setAccessible(true);
            String name = field.getName();
            if (field.isAnnotationPresent(SerializedName.class)) {
                String value = field.getAnnotation(SerializedName.class).value();
                Matcher matcher = PATTERN.matcher(value);
                if (matcher.find()) {
                    name = props.get(matcher.group(1)).toString();
                } else {
                    name = value;
                }
            }
            try {
                if (field.get(foo) != null) {
                    object.addProperty(name, field.get(foo).toString());
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return object;
    }
}

Now you just need to register your custom serializer and you are good to go:

Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class, new FooSerializer()).setPrettyPrinting().create();

Of course there may be better ways to recover the properties file according to the active profile, but the given snippet should be enough to get you going. Also, you need to consider the fact that there may be multiple profiles active at any given time, so if that is your scenario, you need to take it into consideration before recovering the properties.

You don't even need the regex part if you will always want to use the value from the properties. I used a regex to allow both cases.

If something wasn't clear, please let me know and I will try to improve it.

EDIT:

For the deserialization I can't come up with anything very good, so here is an example which I think is far from OK, but gets the job done:

public class FooDeserializer implements JsonDeserializer<Foo> {

    private static final Pattern PATTERN = Pattern.compile("\\$\\{(.*)\\}");
    private static Properties props;
    private static Map<Class, Converter> converterForClass = new HashMap<>();

    static {
        try {
            Resource resource = new ClassPathResource(String.format("/application-%s.properties", System.getProperty("spring.profiles.active")));
            props = PropertiesLoaderUtils.loadProperties(resource);

            converterForClass.put(Integer.TYPE, s -> Integer.parseInt(s.replace("\"", "")));
            converterForClass.put(Double.TYPE, s -> Double.parseDouble(s.replace("\"", "")));
            converterForClass.put(String.class, s -> s);
            converterForClass.put(Long.TYPE, s -> Long.parseLong(s.replace("\"", "")));
            converterForClass.put(Boolean.TYPE, s -> Boolean.parseBoolean(s.replace("\"", "")));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Foo deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {


        Foo foo = new Foo();

        JsonObject jobject = (JsonObject) jsonElement;

        for (Entry entry : jobject.entrySet()) {
            Field field = searchField(entry.getKey().toString());
            if (field != null) {
                field.setAccessible(true);
                try {
                    Object r = converterForClass.get(field.getType()).convert(entry.getValue().toString());
                    field.set(foo, r);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
        return foo;

    }

    private Field searchField(String name) {
        Field[] fields = Foo.class.getDeclaredFields();

        for (Field field : fields) {
            field.setAccessible(true);
            if (field.isAnnotationPresent(SerializedName.class)) {
                String value = field.getAnnotation(SerializedName.class).value();

                Matcher matcher = PATTERN.matcher(value);
                if (value.equals(name)) {
                    return field;
                } else if (matcher.find()) {
                    if (props.get(matcher.group(1)).equals(name)) {
                        return field;
                    }
                }
            } else {
                if (field.getName().equals(name)) {
                    return field;
                }
            }
        }

        return null;
    }

Register the deserializer:

gsonBuilder.registerTypeAdapter(Foo.class, new FooDeserializer());

The problem with the above approach is it will NOT work with nested objects. You will have to some further verifications and implementation.

Comments