Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrofit / enum class / @Json name specified / @FormUrlEncoded API #1813

Open
nikclayton opened this issue Feb 28, 2024 · 1 comment
Open

Comments

@nikclayton
Copy link

[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:

  1. As a separate converter factory, mentioned in the docs?
  2. As an option (default off) to the existing converter factory?
  3. 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.

@JakeWharton
Copy link
Member

I would say that as-written this is fairly specific. It works for enums, but not classes. It also doesn't honor any JsonAdapters 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants