Transformers

Transform field type/value between the source and target classes.

In this guide we will see how we can use transformers to map fields. Field transformers are a way to transform a field from one type to another when mapping it to a destination class. For example, you might want to map a field from a String to a List<String> where the source field is comma delimited. In our example, we will explore this use case, as well as an implicit (default) transformation from Date to Long (milliseconds).

Classes

Like before, we start by defining our source and destination classes:

data class SimpleEntity(
    val creationDate: Date,
    val commaDelimitedString: String, 
)
data class SimpleEntityDisplay(
    val creationDate: Long = 0,
    val stringList: List<String> = emptyList()
)

Creating Transformer

Let's first create our custom StringToListMappingTransformer;

class StringToListMappingTransformer : MappingTransformer<String, List<String>> {
    override fun transform(context: MappingTransformerContext<out String>): List<String>? {
        return context.originalValue?.split(",")
    }
}

The MappingTransformerContext holds all the required data to perform simple and complex transformations. In this example, all we need to do is to take the original value and split it.

Registering Transformers

Since we're using custom transformers, we will have to instantiate ShapeShift using ShapeShiftBuilder and define our two transformers. In ShapeShift, you define a transformer by providing a TransformerRegistration object.

The registration object is used to define the type of the transformer, its instance, and whether it's a default transformer. We will register the DateToLongTransformer as a default transformer, and the StringToListTransformer as a normal transformer.

val shapeShift = ShapeShiftBuilder()
    .withTransformer(DateToLongMappingTransformer(), default = true)
    .withTransformer(StringToListMappingTransformer())
    .build()

Default Transformers

When registering transformers you can indicate wether a transformer is a default transformer. A default transformer of types <A, B> is used when you map a field of type <A> to field of type <B> without specifying a transformer to be used.

ShapeShift comes out of the box with some default transformers. The default transformers are available in the dev.krud.shapeshift.transformer package here.

Examples for default transformers: AnyToStringMappingTransformer, DateToLongMappingTransformer.

Exclude Default Transformers

To exclude the out of the box default transformers just call the excludeDefaultTransformers when creating the ShapeShift instance.

val shapeShift = ShapeShiftBuilder()
    .excludeDefaultTransformers()
    .build()

Using Transformers

Annotations

We can now add our annotations;

@DefaultMappingTarget(SimpleEntityDisplay::class)
data class SimpleEntity(
    @MappedField
    val creationDate: Date,

    @MappedField(transformer = StringToListMappingTransformer::class, mapTo = "stringList")
    val commaDelimitedString: String
)

Note that we did not need to specify a transformer on creationDate since the DateToLongTransformer is a default transformer for the Date type with a Long destination type.

Kotlin DSL

We can create the same mapping with the DSL using the withTransformer function.

val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
    SimpleEntity::creationDate mappedTo SimpleEntityDisplay::creationDate
    SimpleEntity::commaDelimitedString mappedTo SimpleEntityDisplay::stringList withTransformer StringToListMappingTransformer::class
}

The DSL also supports inline transformer. When we don't need to reuse a transformer we can just add its logic to the DSL.

val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
    SimpleEntity::creationDate mappedTo SimpleEntityDisplay::creationDate
    SimpleEntity::commaDelimitedString mappedTo SimpleEntityDisplay::stringList withTransformer {
        it.originalValue?.split(",")
    }
}

Java Builder

We can create the same mapping with the builder using the withTransformer function.

MappingDefinition mappingDefinition = new MappingDefinitionBuilder(SimpleEntity.class, SimpleEntityDisplay.class)
        .mapField("creationDate", "creationDate")
        .mapField("commaDelimitedString", "stringList")
        .withTransformer(StringToListMappingTransformer.class)
        .build();

As the DSL, the builder also supports inline transformer.

MappingDefinition mappingDefinition = new MappingDefinitionBuilder(SimpleEntity.class, SimpleEntityDisplay.class)
        .mapField("creationDate", "creationDate")
        .mapField("commaDelimitedString", "stringList")
        .withTransformer(context -> context.getOriginalValue() != null
                ? Arrays.asList(((String) context.getOriginalValue()).split(","))
                : null)
        .build();

Test

Let's write a test to verify that our mapping is correct;

@Test
fun `test mapping for SimpleEntityDisplay`() {
    val shapeShift = ShapeShiftBuilder()
        .withTransformer(DateToLongTransformer(), default = true)
        .withTransformer(StringToCommaSeparatedStringListTransformer())
        .build()
    val simpleEntity = SimpleEntity(
            Date(),
            "one,two,three"
    )
    val result = shapeShift.map<SimpleEntityDisplay>(simpleEntity)
    expectThat(result.creationDate)
            .isEqualTo(simpleEntity.creationDate.time)
    expectThat(result.stringList)
            .isEqualTo(listOf("one", "two", "three"))
}

Full Example

You can check out the full example here.

Last updated