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: