Sitemap
Expedia Group Technology

Stories from the Expedia Group Technology teams

Expedia Group Technology — Engineering

Polymorphic Deserialization in Spring Data MongoDB: A Reactive approach

6 min readNov 19, 2024

--

An aeriel view of the Golden Gate Bridge
Photo by Edgar Chaparro on Unsplash

The scenario

data class Product(
val _id: String,
val attributes: List<Attribute>
)

sealed class Attribute {
@TypeAlias("NumericAttribute")
data class NumericAttribute(
val value: Int,
val unit: String
) : Attribute()
@TypeAlias("TextAttribute")
data class TextAttribute(
val description: String,
val language: String
) : Attribute()
}

Basic configuration for ReactiveMongoTemplate

@Configuration
@EnableConfigurationProperties(MongoProperties::class)
class ReactiveMongoConfiguration(
val mongoProperties: MongoProperties
) : AbstractReactiveMongoConfiguration() {

override fun getDatabaseName(): String {
return mongoProperties.database
}

override fun reactiveMongoClient(): MongoClient {
return MongoClients.create(mongoProperties.uri)
}

@Bean
override fun reactiveMongoTemplate(
reactiveMongoDatabaseFactory: ReactiveMongoDatabaseFactory,
): ReactiveMongoTemplate {
return ReactiveMongoTemplate(reactiveMongoDatabaseFactory)
}
}
{
"_id": "product_id",
"attributes": [
{
"_class": "NumericAttribute",
"value": 42,
"unit": "kg"
},
{
"_class": "TextAttribute",
"description": "High quality",
"language": "en"
}
]
}

The error

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.example.models.Attribute]: Is it an abstract class?; nested exception is java.lang.InstantiationException

Why this happens

Cache dependency

The solution

Enter the converter

Manual converter

@ReadingConverter
class AttributeConverter : Converter<Document, Attribute?> {

override fun convert(source: Document): Attribute? {
return when (source.getString("_class")) {
"NumericAttribute" -> {
val value = source.getInteger("value")
val unit = source.getString("unit")
Attribute.NumericAttribute(value, unit)
}
"TextAttribute" -> {
val description = source.getString("description")
val language = source.getString("language")
Attribute.TextAttribute(description, language)
}
else -> null
}
}
}

Why manual conversion falls short

Leveraging Jackson for deserialization

@ReadingConverter
class AttributeReadConverter(private val objectMapper: ObjectMapper) : Converter<Document, Attribute?> {
override fun convert(source: Document): Attribute? {
val classType = source.getString("_class")
?: throw IllegalArgumentException("Class type not found in document")

return when (classType) {
"NumericAttribute" -> objectMapper.convertValue(source, Attribute.NumericAttribute::class.java)
"TextAttribute" -> objectMapper.convertValue(source, Attribute.TextAttribute::class.java)
else -> throw IllegalArgumentException("Unknown class type: $classType")
}
}
}
@Bean
fun objectMapper(): ObjectMapper {
return ObjectMapper()
.registerModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}

The final piece: Solving the puzzle

@Configuration
@EnableConfigurationProperties(MongoProperties::class)
class ReactiveMongoConfiguration(
val mongoProperties: MongoProperties
) : AbstractReactiveMongoConfiguration() {

override fun getDatabaseName(): String {
return mongoProperties.database
}
override fun reactiveMongoClient(): MongoClient {
return MongoClients.create(mongoProperties.uri)
}
override fun customConversions() = MongoCustomConversions(
listOf(
AttributeReadConverter() // here we have inserted the converter
)
)
@Bean
override fun reactiveMongoTemplate(
reactiveMongoDatabaseFactory: ReactiveMongoDatabaseFactory,
): ReactiveMongoTemplate {
// Create custom conversions
val conversions = customConversions()

// Set up mapping context
val context = MongoMappingContext()
context.setSimpleTypeHolder(conversions.simpleTypeHolder)
context.afterPropertiesSet()

// Create mapping converter
val converter = MappingMongoConverter(
ReactiveMongoTemplate.NO_OP_REF_RESOLVER, // No operation resolver for ReactiveMongoTemplate
context
)
converter.setCustomConversions(conversions)
converter.setCodecRegistryProvider(databaseFactory) // Set codec registry provider
converter.afterPropertiesSet()

// Finally, configure the converter in ReactiveMongoTemplate
return ReactiveMongoTemplate(reactiveMongoDatabaseFactory, converter)
}
}

No responses yet