kodable

Introduction: Reflection-less Kotlin/JVM json serializer/deserializer
More: Author   ReportBugs   
Tags:

Reflectionless simple json serialization/deserialization library for kotlin-jvm

Usage

Library gives two abilities to deal with json:

  • autogenerated code
  • manually write custom coders/decoder

Features:

  • Generated code does not use any kind of reflection in runtime
  • It's fast in runtime, very fast
  • Compile time check for kodables - if there is no kodable for type we'll get exception while compilation
  • no type erasure in runtime - all kodables called directly without any runtime registry, can pass kodables as arguments anywhere
  • simple way for getting kodable for list of elements

Limitations:

  • Kodable doesn't support default values, only nullability - if property is not exists in json object it becomes null.
  • Kodable does not support generics, wildcards etc. Only List<Type> including nested lists. Used for present json arrays.

Autogenerated code

Simplest and fastest way - just annotate classes or it's constructors and voila!

@Koder generates both encoder and decoder, used for data classes and enums.

@Dekoder generates decoder for trivial classes

@Enkoder generates encoder for trivial classes - can be applied only for inner classes of types needed to be encoded.

Data classes

@Koder
data class User(
    val name: String, // required property in json

    // This annotation says that in json property name is differ from kotlin class property
    @KodableName("surname")
    // nullable types are optional in json - there will not be exception if property is not decoded, it just sets to null
    val givenName: String? 
)

Simplest case - Kodable generates decoder and encoder for all properties in primary constructor. That it simple!

So now we can use generated kodable

val kodable = User::class.kodable()
val user = kodable.dekode(""" { "name": "John" } """)
// user.name = "John"
// user.givenName = null

Enums

@Koder
enum class Gender {
    // If enum can not be decoded from json than uses that value.
    // If default is not specified than json decoding is failed with exception
    @Default 
    unspecified,
    male,
    female;
}

Just assumes that enum's value name is equal to json enum value and presented as string.

val female = Gender::class.kodable().dekode(""" "female" """)
// female = Gender.female
val unknown = Gender::class.kodable().dekode(""" "aaaaaa!!!" """)
// unknown = Gender.unspecified

Trivial classes

@Dekoder
class User(name: String, givenName: String?)

In this example Kodable takes first constructor in class and uses it's signature as Json fields description.

You are able use val/var in constructor if it's primary same as with data classes.

@Dekoder
class User(val name: String,  @KodableName("surname") val givenName: String?, gender: Gender?)
// Last parameter is not `val/var` so it will be decoded from json but you must deal with it by yourself

If not using data class style primary constructor, than you can emulate default values for example or do something else

@Dekoder
class User(val name: String?, @KodableName("surname") givenName: String?) {
    val givenName: String = givenName ?: "Doe"
}

You also can annotate not class itself but it's any constructor.

class User {
    val name: String
    val givenName: String

    constructor (name: String, givenName: String?) {
        this.name = name
        this.givenName = givenName
    }

    @Dekoder
    constructor(fullName: String?) {
        // splitting full name and assign name and givenName
        ...
    }
}

Decoding of trivial classes is much more tricky: you must create inner class and all properties of it will be encoded to json. All properties names are json properties names in json object:

@Dekoder
class User(val name: String?, @KodableName("surname") givenName: String?) {
    val givenName: String = givenName ?: "Doe"

    // this field we don't want to encode
    var gender = Gender.unspecified

    @Enkoder
    // this class must have empty constructor! Others are not prohibited, but not used
    inner class Out {
        val name = this@User.name
        val surname = this@User.givenName
    }
}

This is tricky but gives total control over serialization/deserialization, even asymmetrical.

External classes

Often you need use external classes (java.util.Date as example or third party library classes) in your models and you can't modify and annotate that classes.

You have two options: use annotation @CustomKodable for specific field/property/constructor parameter or use @DefaultKodableForType annotation - mark with it your own kodable realization and it will be used as default.

You can combine this methods - define default kodable, but sometimes use another for specific properties.

Example - we define that all dates are ISO8601 in json:

@DefaultKodableForType(Date::class)
object DateKodable : IKodable<Date> {
    private val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ENGLISH)
    override fun readValue(reader: JSONReader): Date = formatter.parse(reader.readString())
    override fun writeValue(writer: JSONWriter, instance: Date) = writer.writeString(formatter.format(instance))
}

Example of @CustomKodable

@Dekoder
data class Event(val caption: String, @CustomKodable(DateKodable::class) val startDate: Date)

Note: you can use @CustomKodable also for overriding ANY default kodables including generated and even for primitive types

Collections

Just use kodable's property list to get kodable for list of elements of given type

val userList: List<User> = User::class.kodable().list.dekode("""[{"name: "Alice"},{"name": "Bob"}]""")

Advanced usage

Sometimes data for decoding is nested in some JSON entities. To decode them we need define nesting entities and get sub entity from them. It's boring:

{
  "data": {
      "user": { 
          "name": "Alice"
        }
    }
}

leads to

@Dekoder
class UserWrapper(val user: User)

@Dekoder
class DataWrapper(val data: UserWrapper)

For use with other model more wrappers needed :-(

So! Kodable has special class KodablePath describing such nesting - when model is decoded Kodable tries move forward to nested element and then decodes needed model (User in example above)

User::class.kodable().dekode("...", KodablePath(".data.user"))

That's it. Notation is simple: object's properties are accessed via .<property_name> and collections elements are accessed via [<index_in_collection>]

Samples of paths:

".data.user"
".data.items[0]"
"[2]" // in that case root element is JSON array
"data" // root dot can be ommitted

TODO

  • [x] add documentation for KodablePath - helper for skip to subelements
    without describing dummy models
    
  • [ ] maps as collections additionally to List
  • [ ] polymorphysm for sealed/trivial classes
  • [ ] more strong type cheking in compile time
  • [ ] simplify enkoders for trivial classes
Support Me
Apps
About Me
Google+: Trinea trinea
GitHub: Trinea