Moshi DateTime Adapter With Multiple Format Support

Apr 11, 2018

If you are using ThreeTenABP (JSR-310/java.time backport for Android) with Moshi, it will generate the following json format for LocalDateTime

{"date":{"day":10,"month":4,"year":2018},"time":{"hour":3,"minute":34,"nano":115000000,"second":18}}

I assume the above would not happend if you are usung java.time, but I think it's best for us to use a standardize format for datetime such as RFC 3339 / ISO 8601 to ensure compatibility.

We shall write a Moshi adapter for LocalDateTime which support ISO string format.

class LocalDateTimeAdapter {    @ToJson    fun toJson(value: LocalDateTime): String {        return FORMATTER.format(value)    }    @FromJson    fun fromJson(value: String): LocalDateTime {        return FORMATTER.parse(value, LocalDateTime.FROM)    }    companion object {        private val FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME    }}

Using the adapter.

val moshi = Moshi.Builder()        .add(KotlinJsonAdapterFactory())        .add(LocalDateTimeAdapter())        .build()val jsonAdapter = moshi.adapter(LocalDateTime::class.java) val now = LocalDateTime.now(ZoneOffset.UTC)var jsonString = jsonAdapter.toJson(now)// Output: 2018-04-10T03:34:18.115// Output without LocalDateTimeAdapter: {"date":{"day":10,"month":4,"year":2018},"time":{"hour":3,"minute":34,"nano":115000000,"second":18}}val newNow = jsonAdapter.fromJson(jsonString)Log.d(TAG, "$now == $newNow = ${now == newNow}")

There might be cases where we want to support both format, such as an existing format is already exist in production.

To support multiple format, we need to make changes to LocalDateTimeAdapter to use JsonQualifier.

@Retention(AnnotationRetention.RUNTIME)@JsonQualifierinternal annotation class DateStringclass LocalDateTimeAdapter {    @ToJson    fun toJson(@DateString value: LocalDateTime): String {        return FORMATTER.format(value)    }    @FromJson    @DateString    fun fromJson(value: String): LocalDateTime {        return FORMATTER.parse(value, LocalDateTime.FROM)    }    companion object {        private val FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME    }}

Then we need to create another adapter to support multiple format.

If json input is String, LocalDateTimeAdapter shall be used, else the default adapter is used.

By default, we use LocalDateTimeAdapter to write to json.

class MultipleFormatsDateAdapter {    @ToJson    fun toJson(writer: JsonWriter, value: LocalDateTime,                        @DateString stringAdapter: JsonAdapter<LocalDateTime>) {        // default use LocalDateTimeAdapter to write        stringAdapter.toJson(writer, value)    }    @FromJson    fun fromJson(reader: JsonReader, @DateString stringAdapter: JsonAdapter<LocalDateTime>,                          defaultAdapter: JsonAdapter<LocalDateTime>): LocalDateTime? {        return if (reader.peek() === JsonReader.Token.STRING) {            stringAdapter.fromJson(reader)        } else {            defaultAdapter.fromJson(reader)        }    }}

I might not fully understand the dynamics of Moshi at play here, but the following is the explanation by Jesse Wilson

This works because Moshi lets you declare multiple JsonAdapter arguments to the @ToJson and @FromJson methods, and these arguments may be annotated.

It also relies on the way this feature works if the types are the same. Here we’re making a JsonAdapter by delegating to another JsonAdapter. When the types are the same Moshi uses its nextAdapter() feature for composition.

To use the above adapter

val moshi = Moshi.Builder()        .add(KotlinJsonAdapterFactory())        .add(LocalDateTimeAdapter())        .add(MultipleFormatsDateAdapter())        .build()val jsonAdapter = moshi.adapter(LocalDateTime::class.java) val now = LocalDateTime.now(ZoneOffset.UTC)var jsonString = jsonAdapter.toJson(now)// Output ISO String: 2018-04-10T03:34:18.115// Support ISO String as inputvar newNow = jsonAdapter.fromJson(jsonString)Log.d(TAG, "$now == $newNow = ${now == newNow}")// Support JSON format as inputjsonString = "{\"date\":{\"day\":10,\"month\":4,\"year\":2018},\"time\":{\"hour\":3,\"minute\":34,\"nano\":115000000,\"second\":18}}"newNow = jsonAdapter.fromJson(jsonString)Log.d(TAG, "$now == $newNow = ${now == newNow}")

References:

❤️ Is this article helpful?

Buy me a coffee ☕ or support my work via PayPal to keep this space 🖖 and ad-free.

Do send some 💖 to @d_luaz or share this article.

✨ By Desmond Lua

A dream boy who enjoys making apps, travelling and making youtube videos. Follow me on @d_luaz

👶 Apps I built

Travelopy - discover travel places in Malaysia, Singapore, Taiwan, Japan.