Gemini14
Gemini14

Reputation: 6254

Specifying different JSON property names according to API version with Jackson

I need to be able to support multiple API versions at the same time using Jackson to serialize/deserialize objects. I've explored solutions like:

However, each one causes its own problems. @JsonProperty would be a perfect solution if I could add, say, multiple versions with the correct names directly in the annotation:

@JsonProperty(api="1.5", "fname")
@JsonProperty(api="1.6", "firstname")
String firstName;

Over time that might get a big long, but it would certainly be easy to understand. However, it doesn't seem like that's possible.

PropertyNamingStrategy and mixins would also be a good idea. In fact, I tried mixin annotations (e.g., Inherit model with different JSON property names) and they worked, but both of these solutions suffer from one problem. You have to specify and use an ObjectMapper (and possibly ObjectReader/Writer, too) somewhere.

This is a pain, because the hierarchy of objects looks like this:

Entity

|--User

|--Group

|--Credential

etc. Entity contains the common properties, things like name, id, description, state, and API version. Let's say you now do the following:

User user = new User("catherine", "stewardess", "active");
user.setApiVersion(Entity.ApiVersion.V2);
if(user.getVersion() == Entity.ApiVersion.V2) {
    MAPPER.addMixin(Entity.class, EntityMixinV2.class);
}
String userJson = MAPPER.writeValueAsString(user);
User user2 = MAPPER.readValue(userJson);
System.out.println(MAPPER.writeValueAsString(user2));

where MAPPER is just an ObjectMapper defined elsewhere and EntityMixinV2 is something like:

public abstract class EntityMixinV2 {

    @JsonProperty("employmentState")
    String state;
}

to override one of the variables (in this case, state) in User's parent class, Entity. There are several problems with this:

Ideally, I would like to be able to use something like the @JsonProperty example above, because the name changes are the issue, not what I do with variables. I've also considered using the

@JsonSerialize(using = EntitySerializer.class)
@JsonDeserialize(using = EntityDeserializer.class)

annotations, but that would just change the content of the variables, not the property names themselves.

Upvotes: 4

Views: 4330

Answers (1)

Wilson
Wilson

Reputation: 11619

You can create a custom annotation to define a list of possible version of property name for a field and create a custom JacksonAnnotationIntrospector to resolve the property name according to a given version.

The custom annotation will look likes:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface VersioningProperties {    
    Property[] value();

    @interface Property {
        String version();
        String value();
    }
}

and the custom JacksonAnnotationIntrospector will look like:

public class VersioningPropertiesIntrospector {
    private String version;

    public VersioningPropertiesIntrospector(String version) {
        this.version = version;
    }

    @Override
    pubilc PropertyName findNameForSerialization(Annotated a) {
        PropertyName propertyName = findNameFromVersioningProperties(a);
        if (propertyName != null) {
            return propertyName;
        }
        return super.findNameForSerialization(a);
    }

    @Override
    pubilc PropertyName findNameForDeserialization(Annotated a) {
        PropertyName propertyName = findNameFromVersioningProperties(a);
        if (propertyName != null) {
            return propertyName;
        }
        return super.findNameForDeserialization(a);
    }

    private PropertyName findNameFromVersioningProperties(Annotated a) {
        VersioningProperties annotation = a.getAnnotation(VersioningProperties.class);
        if (annotation == null) {
            return null;
        }
        for (Property property : annotation.value()) {
            if (version.equals(property.version()) {
                return new PropertyName(property.value());
            }
        }
        return null;
    }
}

Example of using the annotation:

public class User {
    @VersioningProperties({
        @Property(version = "1.5", value = "fname"),
        @Property(version = "1.6", value = "firstName")
    })
    private String firstName;

    // constructors, getters and setters
}

and an example of using ObjectMapper with the introspector:

User user = new User();
user.setFirstName("catherine");
user.setVersion("1.5");

ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new VersioningPropertiesIntrospector(user.getVersion()));

String userJson = mapper.writeValueAsString(user);
User userRead = mapper.readValue(userJson, User.class);

You may also consider implement a factory to get ObjectMapper by passing the version information.

Upvotes: 3

Related Questions