Your task is to implement a function to serialize a Kotlin object to JSON. The resulting text should include all class member properties. You should support primitive types, iterables (present as an array), and map type (present as an object). You should also support nested objects. Do not use any external libraries. Kotlin's reflection is all you need.
You should support the following annotations:
@SerializationName - can be applied to a property to change its name in the resulting JSON.
@SerializationIgnore - can be applied to a property to ignore it in the resulting JSON.
@SerializationNameMapper - can be applied to a class or property to specify a custom name mapper. The mapper should implement the NameMapper interface. This mapper can be an object declaration or a class with a no-arg constructor.
@SerializationIgnoreNulls - can be applied to a class to ignore all null properties in the resulting JSON.
@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
}
fun serializeToJson(value: Any): String = TODO()
Example usage:
@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"]}
}
This problem can either be solved in the below playground or you can clone kotlin-exercises project and solve it locally. In the project, you can find code template for this exercise in advanced/reflection/JsonSerializer.kt. You can find there starting code, example usage and unit tests.
Once you are done with the exercise, you can check your solution here.
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 = TODO()
@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
}
}
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.