Skip to content

jonpeterson/spring-webmvc-model-versioning

Repository files navigation

Spring MVC Model Versioning

Spring MVC binding for using Jackson Model Versioning Module.

Example

Note: This example is using Groovy for brevity, but it is not required.

Example data model versions

Car data v1

{
  "model": "honda:civic",
  "year": 2016,
  "new": "true"
}

Car data v2

{
  "make": "honda",
  "model": "civic",
  "year": 2016,
  "new": "true"
}

Car data v3

{
  "make": "honda",
  "model": "civic",
  "year": 2016,
  "used": false
}

Define the model POJO and version converters

Create a POJO for the newest version of the data. Using the Jackson Model Versioning Module, annotate the model with the current version and specify the converter class to use when deserializing from a potentially old version of the model to the current version and the converter class to use when serializing from the current version to a potentially old version of the model. Also add a field (or getter/setter methods) that specify the version that the model to be serialized to.

@JsonVersionedModel(currentVersion = '3',
                    toCurrentConverterClass = ToCurrentCarConverter,
                    toPastConverterClass = ToPastCarConverter)
class Car {
    String make
    String model
    int year
    boolean used

    @JsonSerializeToVersion
    String serializeToVersion
}

Create the "up" converter and provide logic for how old versions should be converted to the current version.

class ToCurrentCarConverter implements VersionedModelConverter {
    @Override
    def ObjectNode convert(ObjectNode modelData, String modelVersion,
                           String targetModelVersion, JsonNodeFactory nodeFactory) {

        // model version is an int
        def version = modelVersion as int

        // version 1 had a single 'model' field that combined 'make' and 'model' with a colon delimiter
        if(version <= 1) {
            def makeAndModel = modelData.get('model').asText().split(':')
            modelData.put('make', makeAndModel[0])
            modelData.put('model', makeAndModel[1])
        }

        // version 1-2 had a 'new' text field instead of a boolean 'used' field
        if(version <= 2)
            modelData.put('used', !Boolean.parseBoolean(modelData.remove('new').asText()))
    }
}

Create the "down" converter and provide logic for how the current version should be converted to an old version.

class ToPastCarConverter implements VersionedModelConverter {

    @Override
    def ObjectNode convert(ObjectNode modelData, String modelVersion,
                           String targetModelVersion, JsonNodeFactory nodeFactory) {

        // model version is an int
        def version = modelVersion as int
        def targetVersion = targetModelVersion as int

        // version 1 had a single 'model' field that combined 'make' and 'model' with a colon delimiter
        if(targetVersion <= 1 && version > 1)
            modelData.put('model', "${modelData.remove('make').asText()}:${modelData.get('model').asText()}")

        // version 1-2 had a 'new' text field instead of a boolean 'used' field
        if(targetVersion <= 2 && version > 2)
            modelData.put('new', !modelData.remove('used').asBoolean() as String)
    }
}

Set up a REST endpoint

Configure the Jackson ObjectMapper used by Spring MVC to use the Jackson Model Versioning Module and import VersionedModelResponseBodyAdvice.

@Configuration
@Import(VersionedModelResponseBodyAdvice)
class SpringMvcVersioningConfiguration {

    @Bean
    Jackson2ObjectMapperBuilder objectMapperBuilder() {
        return Jackson2ObjectMapperBuilder.json().modulesToInstall(new VersioningModule())
    }
}

Create a REST endpoint

@RequestMapping(method = RequestMethod.POST,
                path = '/',
                consumes = MediaType.APPLICATION_JSON_VALUE,
                produces = MediaType.APPLICATION_JSON_VALUE)
@VersionedResponseBody(defaultVersion = '2',
                       headerName = 'Model-Version',
                       queryParamName = 'modelVersion')
Car createCar(@RequestBody Car car) {
    return carRepository.insert(car)
}

Test the endpoint

All that's left is to test it out.

def restTemplate = new RestTemplate(
    messageConverters: [
        new MappingJackson2HttpMessageConverter(new ObjectMapper().registerModule(new VersioningModule()))
    ]
)

// POST version 1 JSON and request version 2 JSON response via URL query param
println restTemplate.postForObject(
    'http://localhost:8080/?modelVersion=2',
    '{"model": "honda:civic", "year": 2016, "new": "true", "modelVersion": "1"],
    String
)
// prints '{"make":"honda","model":"civic","year":2016,"new":"true","modelVersion":"2"}'

// POST version 1 JSON and request version 2 JSON response via HTTP header
println restTemplate.exchange(
    'http://localhost:8080/',
    HttpMethod.POST,
    new HttpEntity<String>(
        '{"model": "honda:civic", "year": 2016, "new": "true", "modelVersion": "1"]',
        new LinkedMultiValueMap<String, String>('Model-Version': '2')
    ),
    String
)
// prints '{"make":"honda","model":"civic","year":2016,"new":"true","modelVersion":"2"}'

More Examples

See the tests under src/test/groovy for more.

Compatibility

  • Requires Java 6 or higher
  • Requires Spring 4.2 or higher (tested with Spring 4.2 - 4.3)
  • Requires Jackson 2.2 or higher (uses a version of Jackson Model Versioning Module which is tested with Jackson 2.2 - 2.8).

Getting Started with Gradle

dependencies {
    compile 'com.github.jonpeterson:spring-webmvc-model-versioning:1.0.0'
}

Getting Started with Maven

<dependency>
    <groupId>com.github.jonpeterson</groupId>
    <artifactId>spring-webmvc-model-versioning</artifactId>
    <version>1.0.0</version>
</dependency>

About

Model versioning for Spring MVC.

Resources

License

Stars

Watchers

Forks

Packages

No packages published