Skip to content

Using kotlin TypeId type‐safe ids with Spring

AdiT edited this page May 21, 2024 · 2 revisions

Using with Spring Data

To enable conversion from/to TypeId created identities create converters and use with an UUID database type.

  1. Reading converter:
import earth.adi.typeid.Id
import earth.adi.typeid.TypeId
import org.springframework.core.convert.TypeDescriptor
import org.springframework.core.convert.converter.GenericConverter
import org.springframework.data.convert.ReadingConverter

@ReadingConverter
class IdReadingConverter : GenericConverter {
  override fun getConvertibleTypes(): MutableSet<GenericConverter.ConvertiblePair> {
    return mutableSetOf(GenericConverter.ConvertiblePair(UUID::class.java, Id::class.java))
  }

  override fun convert(source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor): Any {
    val entityType = targetType.resolvableType.resolveGenerics()[0]
    if (targetType.type == Id::class.java) {
      return TypeId.of(entityType, source as UUID)
    } else {
      throw IllegalArgumentException("Unsupported targetType: $targetType")
    }
  }
}
  1. Writing converter:
import earth.adi.typeid.Id
import java.util.UUID
import org.springframework.core.convert.converter.Converter
import org.springframework.data.convert.WritingConverter

@WritingConverter
class IdWritingConverter : Converter<Id<*>, UUID> {
  override fun convert(source: Id<*>): UUID {
    return source.uuid
  }
}

Register the converters:

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration

@Configuration
class DataModuleConfiguration : AbstractJdbcConfiguration() {

  override fun userConverters(): List<*> {
    return listOf(
        IdReadingConverter(),
        IdWritingConverter(),
    )
  }
}

Using with Spring MVC for REST services

To enable typed parameters on the rest controller interfaces, create a formatters configurer and add it to the registry:

import earth.adi.typeid.Id
import earth.adi.typeid.TypeId
import java.util.*
import org.springframework.core.convert.TypeDescriptor
import org.springframework.core.convert.converter.GenericConverter
import org.springframework.format.FormatterRegistry
import org.springframework.stereotype.Component
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Component
class FormattersConfigurer : WebMvcConfigurer {
  override fun addFormatters(registry: FormatterRegistry) {
    registry.addConverter(IdConverter())
  }

  class IdConverter : GenericConverter {
    override fun getConvertibleTypes(): MutableSet<GenericConverter.ConvertiblePair> {
      return mutableSetOf(GenericConverter.ConvertiblePair(String::class.java, Id::class.java))
    }

    override fun convert(
        source: Any?,
        sourceType: TypeDescriptor,
        targetType: TypeDescriptor
    ): Any {
      val entityType = targetType.resolvableType.resolveGenerics()[0]
      if (targetType.type == Id::class.java) {
        return TypeId.parse(entityType, source as String)
      } else {
        throw IllegalArgumentException("Unsupported targetType: $targetType")
      }
    }
  }
}

Then use it like:

import org.springframework.web.bind.annotation.RequestHeader

// ...
@RequestHeader("X-User-Id") userId: UserId,

Spring Jackson Configuration

To enable body ids to json conversions, add the TypeId.jacksonModule(), example configuration:

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.SerializationFeature
import earth.adi.typeid.TypeId
import java.time.Instant
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder

@Configuration
class JsonConfiguration {
  @Bean
  fun jsonCustomizer(): Jackson2ObjectMapperBuilderCustomizer {
    return Jackson2ObjectMapperBuilderCustomizer { builder: Jackson2ObjectMapperBuilder ->
      configureMapperBuilder(builder)
    }
  }

  @Bean
  fun apiObjectMapper(): ObjectMapper {
    return configureMapperBuilder(Jackson2ObjectMapperBuilder()).build()
  }

  companion object {
    private fun configureMapperBuilder(
        builder: Jackson2ObjectMapperBuilder
    ): Jackson2ObjectMapperBuilder {
      return builder
          .modulesToInstall(TypeId.jacksonModule())
          .featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
          .featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
          .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
          .serializationInclusion(JsonInclude.Include.NON_NULL)
          .indentOutput(true)
    }
  }
}