Solution: Object serialization to JSON

fun serializeToJson(value: Any): String = valueToJson(value) private fun objectToJson(any: Any): String { val reference = any::class val classNameMapper = reference .findAnnotation<SerializationNameMapper>() ?.let(::createMapper) val ignoreNulls = reference .hasAnnotation<SerializationIgnoreNulls>() return reference .memberProperties .filterNot { it.hasAnnotation<SerializationIgnore>()} .mapNotNull { prop -> val annotationName = prop .findAnnotation<SerializationName>() val mapper = prop .findAnnotation<SerializationNameMapper>() ?.let(::createMapper) val name = annotationName?.name ?: mapper?.map(prop.name) ?: classNameMapper?.map(prop.name) ?: prop.name val value = prop.call(any) if (ignoreNulls && value == null) { return@mapNotNull null } "\"${name}\": ${valueToJson(value)}" } .joinToString( prefix = "{", postfix = "}", ) } private fun valueToJson(value: Any?): String = when (value) { null, is Number, is Boolean -> "$value" is String, is Char, is Enum<*> -> "\"$value\"" is Iterable<*> -> iterableToJson(value) is Map<*, *> -> mapToJson(value) else -> objectToJson(value) } private fun iterableToJson(any: Iterable<*>): String = any .joinToString( prefix = "[", postfix = "]", transform = ::valueToJson ) private fun mapToJson(any: Map<*, *>) = any.toList() .joinToString( prefix = "{", postfix = "}", transform = { "\"${it.first}\": ${valueToJson(it.second)}" } ) private fun createMapper( annotation: SerializationNameMapper ): NameMapper = annotation.mapper.objectInstance ?: createWithNoargConstructor(annotation) ?: error("Cannot create mapper") private fun createWithNoargConstructor( annotation: SerializationNameMapper ): NameMapper? = annotation.mapper .constructors .find { it.parameters.isEmpty() } ?.call()

Example solution in playground

import org.junit.Test import kotlin.reflect.KClass import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.hasAnnotation import kotlin.reflect.full.memberProperties import kotlin.test.assertEquals fun serializeToJson(value: Any): String = valueToJson(value) private fun objectToJson(any: Any): String { val reference = any::class val classNameMapper = reference .findAnnotation<SerializationNameMapper>() ?.let(::createMapper) val ignoreNulls = reference .hasAnnotation<SerializationIgnoreNulls>() return reference .memberProperties .filterNot { it.hasAnnotation<SerializationIgnore>()} .mapNotNull { prop -> val annotationName = prop .findAnnotation<SerializationName>() val mapper = prop .findAnnotation<SerializationNameMapper>() ?.let(::createMapper) val name = annotationName?.name ?: mapper?.map(prop.name) ?: classNameMapper?.map(prop.name) ?: prop.name val value = prop.call(any) if (ignoreNulls && value == null) { return@mapNotNull null } "\"${name}\": ${valueToJson(value)}" } .joinToString( prefix = "{", postfix = "}", ) } private fun valueToJson(value: Any?): String = when (value) { null, is Number, is Boolean -> "$value" is String, is Char, is Enum<*> -> "\"$value\"" is Iterable<*> -> iterableToJson(value) is Map<*, *> -> mapToJson(value) else -> objectToJson(value) } private fun iterableToJson(any: Iterable<*>): String = any .joinToString( prefix = "[", postfix = "]", transform = ::valueToJson ) private fun mapToJson(any: Map<*, *>) = any.toList() .joinToString( prefix = "{", postfix = "}", transform = { "\"${it.first}\": ${valueToJson(it.second)}" } ) private fun createMapper( annotation: SerializationNameMapper ): NameMapper = annotation.mapper.objectInstance ?: createWithNoargConstructor(annotation) ?: error("Cannot create mapper") private fun createWithNoargConstructor( annotation: SerializationNameMapper ): NameMapper? = annotation.mapper .constructors .find { it.parameters.isEmpty() } ?.call() @SerializationNameMapper(SnakeCaseName::class) @SerializationIgnoreNulls class Creature( val name: String, @SerializationName("att") val attack: Int, @SerializationName("def") val defence: Int, val traits: List<Trait>, val elementCost: Map<Element, Int>, @SerializationNameMapper(LowerCaseName::class) val isSpecial: Boolean, @SerializationIgnore var used: Boolean = false, val extraDetails: String? = null, ) object LowerCaseName : NameMapper { override fun map(name: String): String = name.lowercase() } class SnakeCaseName : NameMapper { val pattern = "(?<=.)[A-Z]".toRegex() override fun map(name: String): String = name.replace(pattern, "_$0").lowercase() } enum class Element { FOREST, ANY, } enum class Trait { FLYING } fun main() { val creature = Creature( name = "Cockatrice", attack = 2, defence = 4, traits = listOf(Trait.FLYING), elementCost = mapOf( Element.ANY to 3, Element.FOREST to 2 ), isSpecial = true, ) println(serializeToJson(creature)) // {"att": 2, "def": 4, // "element_cost": {"ANY": 3, "FOREST": 2}, // "isspecial": true, "name": "Cockatrice", // "traits": ["FLYING"]} } @Target(AnnotationTarget.PROPERTY) annotation class SerializationName(val name: String) @Target(AnnotationTarget.PROPERTY) annotation class SerializationIgnore @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) annotation class SerializationNameMapper(val mapper: KClass<out NameMapper>) @Target(AnnotationTarget.CLASS) annotation class SerializationIgnoreNulls interface NameMapper { fun map(name: String): String } class JsonSerializerTest { @Test fun `should serialize numbers`() { assertEquals("10", serializeToJson(10)) assertEquals("123", serializeToJson(123)) } @Test fun `should serialize string`() { assertEquals("\"ABC\"", serializeToJson("ABC")) assertEquals("\"A B C\"", serializeToJson("A B C")) } @Test fun `should serialize object with string`() { class ExampleClass(val s1: String, val s2: String) assertEquals( "{\"s1\": \"ABC\", \"s2\": \"DEF\"}", serializeToJson(ExampleClass("ABC", "DEF")) ) } @Test fun `should serialize nested objects`() { class Name(val value: String) class Box(val name: Name) assertEquals( "{\"name\": {\"value\": \"ABC\"}}", serializeToJson(Box(Name("ABC"))) ) } @Test fun `should serialize list`() { class ExampleClass(val names: List<String>, val grades: List<Int>) assertEquals( "{\"grades\": [3, 4, 3], \"names\": [\"A\", \"B\", \"C\"]}", serializeToJson(ExampleClass(listOf("A", "B", "C"), listOf(3, 4, 3))) ) } @Test fun `should serialize map`() { class ExampleClass(val grades: Map<String, Int>) assertEquals( "{\"grades\": {\"Alex\": 5, \"Beatrice\": 1}}", serializeToJson(ExampleClass(mapOf("Alex" to 5, "Beatrice" to 1))) ) } @Test fun `should serialize complex object`() { class Creature( val name: String, val attack: Int, val defence: Int, val traits: List<Trait>, val cost: Map<Element, Int>, ) val creature = Creature( name = "Cockatrice", attack = 2, defence = 4, traits = listOf(Trait.FLYING), cost = mapOf( Element.ANY to 3, Element.FOREST to 2 ) ) assertEquals( "{\"attack\": 2, \"cost\": {\"ANY\": 3, \"FOREST\": 2}, \"defence\": 4, \"name\": \"Cockatrice\", \"traits\": [\"FLYING\"]}", serializeToJson(creature) ) } @Test fun `should ignore properties`() { class Creature( @SerializationIgnore val name: String, val attack: Int, val defence: Int, val traits: List<Trait>, val cost: Map<Element, Int>, ) val creature = Creature( name = "Cockatrice", attack = 2, defence = 4, traits = listOf(Trait.FLYING), cost = mapOf( Element.ANY to 3, Element.FOREST to 2 ) ) assertEquals( "{\"attack\": 2, \"cost\": {\"ANY\": 3, \"FOREST\": 2}, \"defence\": 4, \"traits\": [\"FLYING\"]}", serializeToJson(creature) ) } @Test fun `should use different property names`() { class Creature( val name: String, @SerializationName("att") val attack: Int, @SerializationName("def") val defence: Int, val traits: List<Trait>, val cost: Map<Element, Int>, ) val creature = Creature( name = "Cockatrice", attack = 2, defence = 4, traits = listOf(Trait.FLYING), cost = mapOf( Element.ANY to 3, Element.FOREST to 2 ) ) assertEquals( "{\"att\": 2, \"cost\": {\"ANY\": 3, \"FOREST\": 2}, \"def\": 4, \"name\": \"Cockatrice\", \"traits\": [\"FLYING\"]}", serializeToJson(creature) ) } @Test fun `should use class mapper`() { @SerializationNameMapper(SnakeCaseName::class) class Creature( val longName: String, val traitsList: List<Trait>, val elementCost: Map<Element, Int>, val isSpecial: Boolean, var isUserAlready: Boolean = false, ) val creature = Creature( longName = "Cockatrice", traitsList = listOf(Trait.FLYING), elementCost = mapOf( Element.ANY to 3, Element.FOREST to 2 ), isSpecial = true, ) assertEquals( "{\"element_cost\": {\"ANY\": 3, \"FOREST\": 2}, \"is_special\": true, \"is_user_already\": false, \"long_name\": \"Cockatrice\", \"traits_list\": [\"FLYING\"]}", serializeToJson(creature) ) } @Test fun `should use property mapper`() { class Creature( @SerializationNameMapper(SnakeCaseName::class) val longName: String, @SerializationNameMapper(LowerCaseName::class) val traitsList: List<Trait>, @SerializationNameMapper(SnakeCaseName::class) val elementCost: Map<Element, Int>, @SerializationNameMapper(LowerCaseName::class) val isSpecial: Boolean, @SerializationNameMapper(SnakeCaseName::class) var isUserAlready: Boolean = false, ) val creature = Creature( longName = "Cockatrice", traitsList = listOf(Trait.FLYING), elementCost = mapOf( Element.ANY to 3, Element.FOREST to 2 ), isSpecial = true, ) assertEquals( "{\"element_cost\": {\"ANY\": 3, \"FOREST\": 2}, \"isspecial\": true, \"is_user_already\": false, \"long_name\": \"Cockatrice\", \"traitslist\": [\"FLYING\"]}", serializeToJson(creature) ) } @Test fun `should override class mapper with property mapper`() { @SerializationNameMapper(SnakeCaseName::class) class Creature( val longName: String, @SerializationNameMapper(LowerCaseName::class) val traitsList: List<Trait>, val elementCost: Map<Element, Int>, @SerializationNameMapper(LowerCaseName::class) val isSpecial: Boolean, var isUsedAlready: Boolean = false, ) val creature = Creature( longName = "Cockatrice", traitsList = listOf(Trait.FLYING), elementCost = mapOf( Element.ANY to 3, Element.FOREST to 2 ), isSpecial = true, ) assertEquals( "{\"element_cost\": {\"ANY\": 3, \"FOREST\": 2}, \"isspecial\": true, \"is_used_already\": false, \"long_name\": \"Cockatrice\", \"traitslist\": [\"FLYING\"]}", serializeToJson(creature) ) } @Test fun `should override mappers with property name`() { @SerializationNameMapper(SnakeCaseName::class) class Creature( @SerializationName("name") val longName: String, val traitsList: List<Trait>, val elementCost: Map<Element, Int>, @SerializationName("special") val isSpecial: Boolean, var isUserAlready: Boolean = false, ) val creature = Creature( longName = "Cockatrice", traitsList = listOf(Trait.FLYING), elementCost = mapOf( Element.ANY to 3, Element.FOREST to 2 ), isSpecial = true, ) assertEquals( "{\"element_cost\": {\"ANY\": 3, \"FOREST\": 2}, \"special\": true, \"is_user_already\": false, \"name\": \"Cockatrice\", \"traits_list\": [\"FLYING\"]}", serializeToJson(creature) ) } @Test fun `should ignore nulls if annotation used`() { @SerializationIgnoreNulls class CreatureIgnoringNulls( val name: String, val attack: Int?, val defence: Int?, val extraDetails: String?, ) class Creature( val name: String, val attack: Int?, val defence: Int?, val extraDetails: String?, ) val creatureIgnoring = CreatureIgnoringNulls( name = "Cockatrice", attack = null, defence = 4, extraDetails = null, ) assertEquals( "{\"defence\": 4, \"name\": \"Cockatrice\"}", serializeToJson(creatureIgnoring) ) val creature = Creature( name = "Cockatrice", attack = null, defence = 4, extraDetails = null, ) assertEquals( "{\"attack\": null, \"defence\": 4, \"extraDetails\": null, \"name\": \"Cockatrice\"}", serializeToJson(creature) ) } enum class Element { FOREST, ANY, } enum class Trait { FLYING } }