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.
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.
Most helpful comment
It's stated that
Use @JsonQualifier when you need different JSON encodings for the same typeI would add that it can also help when decoding a JSON field that contains different data types. You need an adapter that takes a
JsonReaderand usesJsonReader.peek()to check the data type.That's from my use case, but maybe you can find a more common example.