Moshi: JsonQualifier adapter doesn't get applied

Created on 26 Jan 2016  路  7Comments  路  Source: square/moshi

I try to use Moshi in my Kotlin project which needs to parse a heterogeneous data as strings: it can be values like 5stars, something, true - and I need to parse them all as string. But when Moshi sees true it tries to parse it as boolean and throws an exception when parsing 3rd item:

com.squareup.moshi.JsonDataException: Expected a string but was BOOLEAN at path $.Data.FilterDictionary[5].Items[0].Options[1].Value

So I tried to come up with custom adapter and @JsonQualifier:

data class DictionaryEntry(
  val Options: List<Property>
)

data class Property(
  val Name: String,
  @ForceToString
  val Value: String
)

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class ForceToString

class ForceToStringAdapter {
  @ToJson
  fun toJson(@ForceToString value: String): String = value
  @FromJson @ForceToString
  fun fromJson(value: String): String = throw UnsupportedOperationException()
//  fun fromJson(value: String): String = value
}

I register it:

    val moshi = Moshi.Builder()
      .add(ForceToStringAdapter())
      .build()
    val retrofit = Retrofit.Builder()
      .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
      .addConverterFactory(MoshiConverterFactory.create(moshi))
      .client(client)
      .baseUrl("...")
      .build()

But alas, nothing happens - same error about Boolean and no UnsupportedOperationException() gets thrown as I would expect from the above code...

I don't know if this is some library error or if I am doing something wrong or If this is something Kotlin specific.

Most helpful comment

It's stated that Use @JsonQualifier when you need different JSON encodings for the same type

I would add that it can also help when decoding a JSON field that contains different data types. You need an adapter that takes a JsonReader and uses JsonReader.peek() to check the data type.

For instance, the following JSON:

[
  {
     isAdmin: 1
  },
  {
     isAdmin: true
  }
]
class EnsuresBooleanAdapter {
  @FromJson @EnsuresBoolean String fromJson(JsonReader reader) throws Exception {
    switch(reader.peek()) {
      case JsonReader.Token.NUMBER:
        return reader.nextInt() == 1;
      case JsonReader.Token.BOOLEAN:
        return reader.nextBoolean();
      default:
        reader.skipValue(); // or throw
        return false;
    }
  }

  @ToJson boolean toJson(@EnsuresBoolean boolean b) throws IOException {
    return b;
  }
}

class Model {
  @EnsuresBoolean
  boolean isAdmin = false
}

That's from my use case, but maybe you can find a more common example.

All 7 comments

Figured it out.

I should have used @field:ForceToString in Property class field annotation. Sorry for the noise.

Hmm, actually I still have a problem. Adapter now gets invoked, but it still gives the same error. I guess what happens is that Moshi pre-parses 'true' as 'boolean' and tries to find an adapter which handles booleans. I tried to change fromJson to accept Boolean but now it breaks for first to cases (see description).

I guess I should try writing full custom adapter then...

Try something like this:

  static class ForceToStringJsonAdapter {
    @ToJson void toJson(JsonWriter writer, @ForceToString String s) throws IOException {
    }

    @FromJson @ForceToString String fromJson(JsonReader reader) throws Exception {
    }
  }

You鈥檒l need to use reader.peek() to sample what type is actually in the JSON so you can handle strings or booleans.

Thank you very moshi, this worked! :)

Got the same issue.
Maybe it's worth to mention in the readme?

@lukaspili what would we say?

It's stated that Use @JsonQualifier when you need different JSON encodings for the same type

I would add that it can also help when decoding a JSON field that contains different data types. You need an adapter that takes a JsonReader and uses JsonReader.peek() to check the data type.

For instance, the following JSON:

[
  {
     isAdmin: 1
  },
  {
     isAdmin: true
  }
]
class EnsuresBooleanAdapter {
  @FromJson @EnsuresBoolean String fromJson(JsonReader reader) throws Exception {
    switch(reader.peek()) {
      case JsonReader.Token.NUMBER:
        return reader.nextInt() == 1;
      case JsonReader.Token.BOOLEAN:
        return reader.nextBoolean();
      default:
        reader.skipValue(); // or throw
        return false;
    }
  }

  @ToJson boolean toJson(@EnsuresBoolean boolean b) throws IOException {
    return b;
  }
}

class Model {
  @EnsuresBoolean
  boolean isAdmin = false
}

That's from my use case, but maybe you can find a more common example.

Was this page helpful?
0 / 5 - 0 ratings