Retrofit / enum class / @Json name specified / @FormUrlEncoded API
nikclayton opened this issue · 1 comments
[I've keyword-stuffed the title so that even if you decide not to do this some poor soul in the same situation has a better chance of finding this solution. I've searched through this repo's issues and PRs and couldn't find anything else discussing this]
I'm working with an API that sends JSON in response to requests, but sometimes expects updates to be POSTed with form URL encoding (https://docs.joinmastodon.org/methods/filters/).
There's an interesting wrinkle with this that I've just become aware of. Using that API as an example I have an enum to represent filter contexts:
enum class FilterContext {
@Json(name = "home") HOME,
@Json(name = "notifications") NOTIFICATIONS,
@Json(name = "public") PUBLIC,
@Json(name = "thread") THREAD,
@Json(name = "account") ACCOUNT,
}
This works exactly as you would expect on receiving JSON, ... "context": "home" ...
deserializes to FilterContext.HOME
correctly.
Naively you might expect that if Moshi's converter factory is correctly added to Retrofit then calling through to an API defined like this:
@FormUrlEncoded
@POST("api/v2/filters")
suspend fun createFilter(
@Field("title") title: String,
@Field("context[]") context: List<FilterContext>, // <-- FilterContext used here
@Field("filter_action") filterAction: String,
@Field("expires_in") expiresInSeconds: Int?,
): NetworkResult<Filter>
would cause Retrofit to also use the @Json
name
property to serialise the FilterContext
.
But of course it doesn't, because @FormUrlEncoded
is not JSON, so the enum values are sent as uppercase and rejected by the server.
Since I'm loathe to repeat an identical lowercase string representation of the enum I wrote this and added it as an additional converter factory when creating the Retrofit singleton in my app.
It's a singleton converter and converterfactory that uses the value of the @Json(name = ...)
property if it exists on the enum, falling back to the normal string representation if it doesn't.
object EnumConstantConverterFactory : Converter.Factory() {
object EnumConstantConverter : Converter<Enum<*>, String> {
override fun convert(enum: Enum<*>): String {
return try {
enum.javaClass.getField(enum.name).getAnnotation(Json::class.java)?.name
} catch (_: Exception) {
null
} ?: enum.toString()
}
}
override fun stringConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit,
): Converter<Enum<*>, String>? {
return if (type is Class<*> && type.isEnum) EnumConstantConverter else null
}
}
[Creating a Moshi adapter for the type and calling toJson
on the adapter doesn't work for this use case because then the enum value is wrapped in quotes -- correct for JSON, not correct here]
Would the Moshi team be interested in bundling this with Moshi? Either:
- As a separate converter factory, mentioned in the docs?
- As an option (default off) to the existing converter factory?
- Mentioning this as an example of how Moshi's annotations can be used in adjacent contexts?
Absolutely no problem if not; at the very least I hope this writeup is useful to anyone else who encounters a similar issue.
I would say that as-written this is fairly specific. It works for enums, but not classes. It also doesn't honor any JsonAdapter
s one might be using for enums installed in a Moshi
instance.
In Retrofit one of its samples is taking an arbitrary request body converter and automatically adapting it to work as string converter: https://github.com/square/retrofit/blob/ae8c4ee3c7f20bcdaafa64d32541c1f00c2f696c/samples/src/main/java/com/example/retrofit/JsonQueryParameters.java. The sample uses Gson and keys on a @Json
annotation, but it will work for any string-based request body converter. This works for classes, but if you try and serialize just an enum you'll get a quoted value, because that's what JSON demands for strings.
I'm not sure there's a perfect solution here that I would feel comfortable shipping in an artifact. I'd be happy to accept something like what you wrote to Retrofit's samples, though.