1. Overview
YAML (YAML Ain’t Markup Language) is a human-readable data serialization format that is commonly used for configuration files. The official Kotlin library, kotlinx-serialization, doesn’t directly support the YAML format. However, there are a few community-backed libraries, such as kaml and YamlKt, that use kotlinx-serialization internally to support the YAML format.
In this article, we’ll learn the deserialization process of reading YAML content in Kotlin.
2. Dependency Setup
First, let’s add the kaml-jvm dependency to our project’s pom.xml:
<dependency>
<groupId>com.charleskorn.kaml</groupId>
<artifactId>kaml-jvm</artifactId>
<version>0.58.0</version>
</dependency>
Next, let’s also add the yamlkt-jvm dependency:
<dependency>
<groupId>net.mamoe.yamlkt</groupId>
<artifactId>yamlkt-jvm</artifactId>
<version>0.13.0</version>
</dependency>
Lastly, we must configure the serialization plugin within the Kotlin compilation plugin:
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<compilerPlugins>
<plugin>kotlinx-serialization</plugin>
</compilerPlugins>
</configuration>
</plugin>
</plugins>
</build>
We must use the latest compatible versions of libraries and plugins.
3. Reading Scalar Values
Scalar values such as strings, numbers, booleans, and nulls are the simplest forms of data in YAML. In this section, let’s learn how to read scalar values.
3.1. With kaml
Let’s create an instance of the com.charleskorn.kaml.Yaml class with the library’s default behavior:
val yamlDefault = com.charleskorn.kaml.Yaml.default
We’re going to reuse this instance in the subsequent sections, too.
Now, let’s use the decodeFromString() method to decode the value of scalarStringYamlStr:
val scalarStringYamlStr = "Hello!"
val parsedData: String = yamlDefault.decodeFromString(scalarStringYamlStr)
assertEquals("Hello!", parsedData)
It parsed it correctly.
Alternatively, we can use the parseToYamlNode() function to parse the contents into a YamlScalar value:
val parsedData: YamlScalar = yamlDefault.parseToYamlNode(scalarStringYamlStr).yamlScalar
assertEquals("k1", parsedData.content)
We used the yamlScalar property of the YamlNode to extract the scalar value. Further, we used the content property of YamlScalar to extract the contents of the parsedData object.
Similarly, we can parse other scalar values, such as numbers, booleans, and nulls.
3.2. With YamlKt
To use YamlKt to deserialize YAML content, we must create an instance of the net.mamoe.yamlkt.Yaml class:
val yamlDefault = net.mamoe.yamlkt.Yaml.Default
We used the library’s default configuration to create yamlDefault.
Like earlier, we can use the decodeFromString() method to get the contents of the scalarStringYamlStr string:
val scalarStringYamlStr = "Hello!"
val parsedData: String = yamlDefault.decodeFromString(scalarStringYamlStr)
assertEquals("Hello!", parsedData)
It looks like we got this one right!
Additionally, it’s important to note that the Yaml class from both libraries implements the SerialFormat interface, which defines the decodeFromString() function. So, we’re using a library-specific implementation of decodeFromString() to parse the scalar values.
4. Reading Lists
In this section, we’ll learn how to read a collection of items using YAML lists data type.
4.1. Block Style and Flow Style
We can write the list of items in two formats, namely, block style and flow style.
Let’s use the block style to specify a list of numbers in the numberListYamlStr string:
val numberListYamlStr =
"""
- 123
- 456
"""
We used the triple-quoted string to specify the multi-line strings. Further, we’d usually prefer this representation for longer lists, as keeping one item per line is more readable.
Alternatively, we can use the flow style to specify the list of numbers using the double-quoted string:
val numberListYamlStr = "[123,456]"
Although we can fit all the items in a single line, it’d become unreadable for longer lists. So, we shall restrict this usage to smaller lists.
4.2. With kaml
First, let’s use the decodeFromString() method to read the list of items into the Kotlin List of Ints:
val parsedData: List<Int> = yamlDefault.decodeFromString(numberListYamlStr)
We can verify the value of the parsedData variable against the list of expected numbers:
assertEquals(listOf(123, 456), parsedData)
Next, let’s use the parseToYamlNode() method to read the list of items into a YamlList value:
val parsedData: YamlList = yamlDefault.parseToYamlNode(numberListYamlStr).yamlList
We used the yamlList property of YamlNode to extract the YamlList value.
Lastly, let’s verify the contents of the YamlList value object by extracting the individual scalar values:
val item1 = parsedData[0].yamlScalar.content
assertEquals(123, item1.toInt())
val item2 = parsedData[1].yamlScalar.content
assertEquals(456, item2.toInt())
We got the correct results.
4.3. With YamlKt
With YamlKt, we can use the decodeFromString() method to extract the list of items, just like we did using kaml. However, the Yaml class in this library supports an alternative to use the decodeListFromString() method to parse the list of items.
Let’s read the list of numbers into a Kotlin List:
val parsedData: List<Any?> = yamlDefault.decodeListFromString(numberListYamlStr)
Further, let’s verify that we’ve got the correct results after parsing:
assertEquals(listOf(123, 456), parsedData)
Fantastic! It worked as expected.
5. Reading Maps
In this section, we’ll learn how to read key-value mappings from YAML content in Kotlin.
5.1. With kaml
Let’s define the mapWithScalarValuesYamlStr variable to store the YAML string:
val mapWithScalarValuesYamlStr =
"""
k1: v1
k2: 123
k3: false
k4: 12.3
k5: 1.2e1
k6: ~
"""
In the mapping, we’ve defined heterogeneous value types, such as string, number, boolean, float, etc.
Now, let’s read the mapping into the YamlMap value object:
val yamlMapNode: YamlMap = yamlDefault.parseToYamlNode(mapWithScalarValuesYamlStr).yamlMap
We used the yamlMap property to extract the contents of YamlMap from the intermediate YamlNode object.
Lastly, let’s verify the individual key-value pairs from the parsing:
assertEquals("v1", yamlMapNode.getScalar("k1")!!.content)
assertEquals(123, yamlMapNode.getScalar("k2")!!.toInt())
assertEquals(false, yamlMapNode.getScalar("k3")!!.toBoolean())
assertEquals(12.3f, yamlMapNode.getScalar("k4")!!.toFloat())
assertEquals(12.0f, yamlMapNode.getScalar("k5")!!.toFloat())
assertTrue(yamlMapNode.get<YamlNode>("k6")!! is YamlNull)
We used the getScalar() method to extract the YamlScalar content. After that, we used the content property for the string value and type-specific methods, such as toInt(), toBoolean(), toFloat(), and YamlNull(), to get the content of the scalar value.
Additionally, we’ve used the not-null operator (!!) to guarantee that we’re expecting non-null values.
5.2. With YamlKt
Using the YamlKt library, we can use the decodeFromString() method to decode the Yaml mapping into a YamlMap object:
val parsedData: YamlMap = yamlDefault.decodeFromString(mapWithScalarValuesYamlStr)
Further, let’s verify the parsed values using the datatype-specific getter methods:
assertEquals("v1", parsedData.getString("k1"))
assertEquals(123, parsedData.getInt("k2"))
assertEquals(false, parsedData.getPrimitive("k3"))
assertEquals(12.3f, parsedData.getFloat("k4"))
assertEquals(12.0f, parsedData.getFloat("k5"))
assertTrue(parsedData["k6"] is YamlNull)
We must note that YamlNull is an object representation for the null value.
Alternatively, YamlKt also offers the decodeMapFromString() method to read the mapping directly into a Kotlin Map:
val parsedData: Map<String?, Any?> = yamlDefault.decodeMapFromString(mapWithScalarValuesYamlStr)
Lastly, let’s verify that our approach worked correctly:
assertEquals("v1", parsedData["k1"])
assertEquals(123, parsedData["k2"])
assertEquals(false, parsedData["k3"])
assertEquals(12.3, parsedData["k4"])
assertEquals(12.0, parsedData["k5"])
assertEquals(null, parsedData["k6"])
Great! It looks like we nailed this one!
6. Reading With User-Defined Schemas
In this section, we’ll learn how to deserialize YAML content into user-defined schemas.
6.1. YAML Document and Schema
Let’s look at the sample users.yaml document:
users:
- name: Alice
age: 30
address:
city: New York
country: USA
- name: Bob
age: 35
address:
city: London
country: UK
Now, let’s define the Address data class with city and country properties:
@Serializable
data class Address(
val city: String,
val country: String,
)
Further, we can define the User data class with name, age, and address properties:
@Serializable
data class User(
val name: String,
val age: Int,
val address: Address
)
Lastly, we’ve got the Users data class with users property representing a list of User objects:
@Serializable
data class Users(
val users: List<User>
)
We marked each of them with @Serializable annotation to mark them ready for serialization and deserialization.
6.2. verifyUsers() Function
From a reusability perspective, let’s define a helper function, verifyUsers(), to verify the deserialization process:
fun verifyUsers(users: Users) {
assertNotNull(users)
val user1 = users.users[0]
assertNotNull(user1)
assertEquals("Alice", user1.name)
assertEquals(30, user1.age)
assertEquals("New York", user1.address.city)
assertEquals("USA", user1.address.country)
val user2 = users.users[1]
assertNotNull(user2)
assertEquals("Bob", user2.name)
assertEquals(35, user2.age)
assertEquals("London", user2.address.city)
assertEquals("UK", user2.address.country)
}
We’ll use this in the following sections to validate the deserialization of the YAML string from users.yaml.
6.3. With kaml
Let’s define the getUsersUsingUsersSerializer() function to read YAML content into the Users object:
fun getUsersUsingUsersSerializer(fileContent: String): Users {
val yaml = Yaml(configuration = YamlConfiguration(strictMode = false))
val data = yaml.decodeFromString<Users>(fileContent)
return data
}
We defined an instance of the Yaml class and used the decodeFromString() method for parsing.
Further, let’s call the getUsersUsingUsersSerializer() and verify the results:
val users = getUsersUsingUsersSerializer(fileContent)
verifyUsers(users)
It works fine!
6.4. With YamlKt
Similarly, we can define the getUsersUsingUsersSerializer() for parsing using the YamlKt library:
fun getUsersUsingUsersSerializer(fileContent: String): Users {
val users: Users = Yaml.Default.decodeFromString(Users.serializer(), fileContent)
return users
}
It’s important to note that the Yaml class is different for the two libraries.
Lastly, let’s verify our approach and corresponding results:
val users = getUsersUsingUsersSerializer(fileContent)
verifyUsers(users)
Excellent! We’ve got it working.
7. Conclusion
In this article, we learned to read YAML content using the kaml and YamlKt libraries. Further, we explored the deserializeFromString() method in detail by using it to deserialize different datatypes supported in a YAML string.
As always, the code from this article is available over on GitHub.