with

fun with(vararg decoderFunctions: Pair<KClass<*>, TomlDecoder.(targetType: KType, tomlValue: TomlValue) -> Any?>): TomlDecoder

Returns a copy of the target TOML decoder, extended with zero or more additional custom decoder functions. A custom decoder function is a function from a TomlValue and a KType representing a target type, to that target type. Custom decoder functions are associated with a KClass representing that target type.

When a TOML value is decoded to some target type, the decoder will look for all decoder functions associated with that type. All decoder functions matching that type are then tried in the order they were registered with the decoder, from newest to oldest. I.e. for some decoder D = TomlDecoder.default.with(T to A).with(T to B), B will always be tried before A when trying to decode values of type T.

A decoder function can signal that they are unable to decode their given input by calling pass. When this happens, the decoder will go on to try the next relevant decoder, if any.

As an example, to decode a TOML document allowing integers to be decoded into Kotlin strings:


val myDecoder = TomlDecoder.default.with(
String::class to { _, tomlValue ->
(tomlValue as? TomlValue.Integer)?.let { tomlValue.value.toString() } ?: pass()
}
)
val result = TomlValue.from(Path.of("path", "to", "file.toml")).decode(myDecoder)

Binding decoder functions to a KClass rather than a KType, while allowing the decoder function to access that KType, allows for more fine-grained control over deserialization. Let's say, for instance, that you have a custom data structure, generic in its elements, that you want to decode TOML values into.

If a decoder function was bound to a KType, you would need to register one decoder function for MyDataStructure, one for MyDataStructure, etc. - a lot of unnecessary boilerplate.

If a decoder function was bound to a KClass and did not have access to the corresponding KType, you would have no way of knowing the type of the elements of the data structure. You would instead be forced to rely on the default decoding of TOML values - TomlValue.Integer into Long, TomlValue.Map into Map, and so on - an unacceptable loss of functionality.

A decoder function with access to the target type's KType, bound to the target type's KClass gets the best of both worlds. As an example, here is how you would create a custom decoder function for the generic data structure used in the above paragraphs.


val myDecoder = TomlDecoder.default.with(
MyDataStructure::class to { kType, tomlValue ->
(tomlValue as? TomlValue.List)?.let {
val myDataStructure = MyDataStructure<Any>()
tomlValue.forEach {
it.convert(this, kType.arguments.single().type!!)
myDataStructure.add(convertedElement)
}
myDataStructure
} ?: pass()
}
)
val result = TomlValue.from(Path.of("path", "to", "file.toml")).decode(myDecoder)


inline fun <T : TomlValue, R> with(crossinline decoderFunction: TomlDecoder.(T) -> R): TomlDecoder

Returns a copy of the receiver TOML decoder extended with a single custom decoder function, without having to manually specify its target type.

A decoder function registered this way may specify a more specific argument type than TomlValue. If it does, the function will only try to handle inputs of that specific type, automatically passing the input to the next decoder function in line if the input is of any other type.

If you care about the performance of this operation, the explicitly typed overload of this function is significantly faster when registering several decoder functions at the same time.


inline fun <T : TomlValue, R> with(crossinline decoderFunction: TomlDecoder.(targetType: KType, T) -> R): TomlDecoder

Returns a copy of the receiver TOML decoder extended with a single custom decoder function, without having to manually specify its target type. The custom decoder function may make decoding decisions based on the KType corresponding to the decoder target type.