I'm trying to write a null-safe String adapter that will serialize this JSON {"nullString": null} into this: Model(nullString = "") so that any JSON with a 'null' value that I expect to be a String will be replaced with "" (assuming there exists a data class like this: data class Model(val nullString: String))
I wrote a custom adapter to try and handle this:
class NullStringAdapter: JsonAdapter<String>() {
#FromJson
override fun fromJson(reader: JsonReader?): String {
if (reader == null) {
return ""
}
return if (reader.peek() == NULL) "" else reader.nextString()
}
#ToJson
override fun toJson(writer: JsonWriter?, value: String?) {
writer?.value(value)
}
}
...in an attempt to solve this parsing error:
com.squareup.moshi.JsonDataException: Expected a name but was NULL at path $.nullString
Moshi parsing code:
val json = "{\"nullString\": null}"
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(NullStringAdapter())
.build()
val result = moshi.adapter(Model::class.java).fromJson(configStr)
What am I missing here? Still new to moshi so any help is appreciated!
The immediate problem is the missing reader.nextNull() call to consume the null value.
There are a couple other cleanup things you can do here, too.
With #FromJson, implementing JsonAdapter is unnecessary.
Also, the JsonReader and JsonWriter are not nullable.
object NULL_TO_EMPTY_STRING_ADAPTER {
#FromJson fun fromJson(reader: JsonReader): String {
if (reader.peek() != JsonReader.Token.NULL) {
return reader.nextString()
}
reader.nextNull<Unit>()
return ""
}
}
and use add the adapter:
val moshi = Moshi.Builder()
.add(NULL_TO_EMPTY_STRING_ADAPTER)
.add(KotlinJsonAdapterFactory())
.build()
Related
TL;DR
For a json string containing ...,field=,..., Gson keeps throwing JsonSyntaxException. What can I do?
The Case
I have to communicate with a 3rd api, Which tends to provide data like this:
{
"fieldA": "stringData",
"fieldB": "",
"fieldC": ""
}
However, In my app project, it turns out to read like this:
val jsonString = "{fieldA=stringData,fieldB=,fieldC=}"
The Problem
I tried using the standard method to deserialize it:
val jsonString = "{fieldA=stringData,fieldB=,fieldC=}"
val parseJson = Gson().fromJson(jsonString, JsonObject::class.java)
assertEquals(3, parseJson.size())
But it results in a Exception:
com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Unexpected value at line 1 column 28 path $.fieldB
The Solutions That Don't Work
I have tried so many solutions, none of them works. Including:
Setup a custom data class and set value to nullable
data class DataExample(
val fieldA: String?,
val fieldB: String?,
val fieldC: String?,
)
val parseToObject = Gson().fromJson(jsonString, DataExample::class.java)
Using JsonElement instead:
data class DataExample(
val fieldA: JsonElement,
val fieldB: JsonElement,
val fieldC: JsonElement,
)
val parseToObject = Gson().fromJson(jsonString, DataExample::class.java)
Applying a Deserializer:
class EmptyToNullDeserializer<T>: JsonDeserializer<T> {
override fun deserialize(
json: JsonElement, typeOfT: Type, context: JsonDeserializationContext
): T? {
if (json.isJsonPrimitive) {
json.asJsonPrimitive.also {
if (it.isString && it.asString.isEmpty()) return null
}
}
return context.deserialize(json, typeOfT)
}
}
data class DataExample(
#JsonAdapter(EmptyToNullDeserializer::class)
val fieldA: String?,
#JsonAdapter(EmptyToNullDeserializer::class)
val fieldB: String?,
#JsonAdapter(EmptyToNullDeserializer::class)
val fieldC: String?,
)
val parseToObject = Gson().fromJson(jsonString, DataExample::class.java)
or using it in GsonBuilder:
val gson = GsonBuilder()
.registerTypeAdapter(DataExample::class.java, EmptyToNullDeserializer<String>())
.create()
val parseToObject = gson.fromJson(jsonString, DataExample::class.java)
What else can I do?
It is not a valid JSON. You need to parse it by yourself. Probably this string is made by using Map::toString() method.
Here is the code to parse it into Map<String, String>
val jsonString = "{fieldA=stringData,fieldB=,fieldC=}"
val userFieldsMap = jsonString.removeSurrounding("{", "}").split(",") // split by ","
.mapNotNull { fieldString ->
val keyVal = fieldString.split("=")
// check if array contains exactly 2 items
if (keyVal.size == 2) {
keyVal[0].trim() to keyVal[1].trim() // return#mapNotNull
} else {
null // return#mapNotNull
}
}
.toMap()
It turns out that, like #frc129 and many others said, it is not an valid JSON.
The truth is however, Gson handles more situation than JSON should be, like the data below:
val jsonString = "{fieldA=stringData,fieldB=s2,fieldC=s3}"
val parseJson = Gson().fromJson(jsonString, JsonObject::class.java)
// This will NOT throw exception, even the jsonString here is not actually a JSON string.
assertEquals(3, parseJson.size())
assertEquals("stringData", parseJson["fieldA"].asString)
assertEquals("s2", parseJson["fieldB"].asString)
assertEquals("s3", parseJson["fieldC"].asString)
Further investigation indicates that -- the string mentioned here and in the question -- is more like a Map to string.
I got a bit misunderstanding with GSON dealing with Map. That should be treat as a extra handy support, but not a legal procedure. In short, it is not supposed to be transformed, and data format should be fixed. I'll go work with server and base transformation then.
Just leave a note here. If someone in the future want some quick fix to string, you may take a look at #frc129 answer; however, the ideal solution to this is to fix the data provider to provide "the correct JSON format":
val jsonString = "{\"fieldA\":\"stringData\",\"fieldB\":\"\",\"fieldC\":\"\"}"
val parseJson = Gson().fromJson(jsonString, JsonObject::class.java)
assertEquals(3, parseJson.size())
assertEquals("stringData", parseJson["fieldA"].asString)
assertEquals("", parseJson["fieldB"].asString)
assertEquals("", parseJson["fieldC"].asString)
Can I convert data when parsing it from json to MutableLiveData and how if it's possible?
#TypeConverter
fun stringToDatas(string: String) :MutableLiveData<HashMap<String,Any>> {
val liveData = object : TypeToken<MutableLiveData<HashMap<String, Any>>>() {
//here i guess should return it.
//and how can i return it from string to MutableLiveData
}.type
return Gson().fromJson(string, liveData)
}
Thank you.
It could be done this way
#TypeConverter
fun stringToDatas(string: String) :MutableLiveData<HashMap<String,Any>> {
val map= object : TypeToken<HashMap<String, Any>>() {}.type
return MutableLiveData<HashMap<String,Any>>(Gson().fromJson(string, map))
}
UPDATE
#TypeConverter
fun datasToString(liveData: MutableLiveData<HashMap<String,Any>>): String {
return gson.toJson(liveData.value)
}
I started working with moshi a couple of weeks ago, so maybe I am missing something trivial, but I spent already quite a bit of time trying to fix this without success, so here is my question.
Having the following reproducible code:
fun main() {
val moshi = Moshi.Builder().add(OptionalAdapter).build()
val objectToSerialize = DummyObject()
val json = moshi.adapter(DummyObject::class.java).serializeNulls().toJson(objectToSerialize)
println(json)
}
#JsonClass(generateAdapter = true)
data class DummyObject(val value: Int=123, val someNullable: String? = null,
val someNotPresent: Optional<String> = Optional.NotPresent,
val somePresent: Optional<String> = Optional.Present("aaaa"))
class OptionalAdapter<T>(private val valueAdapter: JsonAdapter<T>) : JsonAdapter<Optional<T>>() {
#Suppress("UNCHECKED_CAST")
override fun fromJson(reader: JsonReader) = Optional.Present(valueAdapter.fromJson(reader) as T)
override fun toJson(writer: JsonWriter, value: Optional<T>?) {
when (value) {
is Optional.NotPresent -> writer.nullValue()
is Optional.Present -> valueAdapter.serializeNulls().toJson(writer, value.value)
}
}
companion object Factory : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
return if (Types.getRawType(type) == Optional::class.java && annotations.isEmpty()) {
val valueType = if(type is ParameterizedType) {
type.actualTypeArguments.get(0)
} else {
//Should not happen
throw IllegalArgumentException()
}
return OptionalAdapter(moshi.adapter<Any>(valueType).nullSafe())
} else {
null
}
}
}
}
sealed class Optional<out T> {
val provided get() = this !is NotPresent
abstract val value: T
object NotPresent : Optional<Nothing>() {
// have the IDE raise an error if the user knows a type is missing but still tries to access a value
#Deprecated(
"Cannot access a missing value",
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("TODO(\"value is missing\")")
)
override val value: Nothing
get() = error("cannot access provided field")
}
data class Present<out T>(override val value: T) : Optional<T>()
}
I would like to serialize as {"value":123,"someNullable":null,"somePresent":"aaaa"} instead of {"value":123,"someNullable":null,"someNotPresent":null,"somePresent":"aaaa"}, which is what is doing now.
Basically, I want to skip the serialization in case the type is Optional.NotPresent. Any suggestion?
The solution I ended up with:
override fun toJson(writer: JsonWriter, value: Optional<T>?) {
when (value) {
is Optional.NotPresent -> {
val wasSerializeNulls = writer.serializeNulls
writer.serializeNulls = false
try {
writer.nullValue()
} finally {
writer.serializeNulls = wasSerializeNulls
}
}
is Optional.Present -> valueAdapter.serializeNulls().toJson(writer, value.value)
}
}
I'm trying to parse JSON data to a class but gson.fromJson(response, bitt::class.java) keeps returning null.
class bitt(#SerializedName("result")val result: String) {
val someVal: String = "string"
fun method() {
print("something")
}
}
val response: String = "{'success':true,'message':'','result':'Im a sult'}"
println(response)
val gson = Gson()
val ticker = gson.fromJson(response, bitt::class.java)
println(ticker)
What am I doing wrong here?
JSON always uses double quotes ", not single quotes '. Your response uses single quotes, so it is not valid JSON.
As in many other languages, you can use \" to put a double quote in a string literal:
val response: String = "{\"success\":true,\"message\":\"\",\"result\":\"I'm a result\"}"
change to Data Class instead of Class
example from your code:
data class bitt(val result: String = "") {
val someVal: String = "string"
fun method() {
print("something")
}
}
I guess it takes long time before you get the result back
so the ticker still remain null
you can use kotlin coroutines to handle it.
or simply use callback like this
data class bitt(val result: String = "") {
val someVal: String = "string"
fun method() {
print("something")
}
}
fun getTicker(response: String, onComplete: (bitt) -> Unit) {
val ticker = Gson().fromJson(response, bitt::class.java)
onComplete(ticker)
}
val response: String = "{'success':true,'message':'','result':'Im a sult'}"
println(response)
getTicker(response){ println(it) }
then you might need to use Coroutine
https://github.com/Kotlin/kotlinx.coroutines
data class bitt(val result: String = "") {
val someVal: String = "string"
fun method() {
print("something")
}
}
suspend fun getTicker(response: String) = Gson().fromJson(response, bitt::class.java)
fun yourMethod() {
val response: String = "{'success':true,'message':'','result':'Im a sult'}"
println(response)
CoroutineScope(IO).launch {
val ticker = getTicker(response)
println(ticker)
}
}
KotlinConf 2017 - Introduction to Coroutines by Roman Elizarov
I have a lot of code like this, it is all the same except for the type PositionJson, it could be AnotherJson or FooJson or BarJson
Is there some way I can exctract all this code into one function that I can somehow pass into it the type? So that I don't have several of these big blocks of almost identical code littering my class?
I'm not sure if this is possible or not, just thought I'd ask because it would be nice to do...
/**
* #return the _open_ [PositionJson]s
*/
val positions: Array<PositionJson>?
#Throws(AccountsAPIException::class)
get() {
val service = constructServiceURL(POSITIONS, null, true)
try {
val messageJson = mapper.readValue<MessageJson<Array<PositionJson>>>(
callURL(service),
object: TypeReference<MessageJson<Array<PositionJson>>>() {
})
val error = messageJson.error
if (error != null) throw AccountsAPIException(error.errorCode, error.description)
return messageJson.data
} catch (e: Exception) {
throw AccountsAPIException(e)
}
}
You can do what you want with generics. However, to use generics we first need to extract that giant block of code into a method:
val positions: Array<PositionJson>? get() = getPositions()
fun getPositions(): Array<PositionJson>? {
...
}
We haven't solved the problem, but now we're in a position to be able to solve it by making getPositions generic (note that I also rename the function):
val positions: Array<PositionJson> get() = getArrayOf<PositionJson>()
// thanks to type inference I can omit the type on getArrayOf if desired:
val positions: Array<PositionJson> get() = getArrayOf()
fun <T> getArrayOf(): Array<T>? {
val service = constructServiceURL(POSITIONS, null, true)
try {
val messageJson = mapper.readValue<MessageJson<Array<T>>>(
callURL(service),
object: TypeReference<MessageJson<Array<T>>>() {
})
val error = messageJson.error
if (error != null) throw AccountsAPIException(error.errorCode, error.description)
return messageJson.data
} catch (e: Exception) {
throw AccountsAPIException(e)
}
}
Perfect! Except this won't compile thanks to type erasure. But we can fix this too by making the function inline and making the type parameter reified:
inline fun <reified T: Any> getArrayOf(): Array<T>? {
...
}
And that should do it. Now you can reuse this function as needed:
val positions: Array<PositionJson>? get() = getArrayOf()
val persons: Array<PersonJson>? get() = getArrayOf()
val bananas: Array<BananaJson>? get() = getArrayOf()
inline fun <reified T: Any> getArrayOf(): Array<T>? {
val service = constructServiceURL(POSITIONS, null, true)
try {
val messageJson = mapper.readValue<MessageJson<Array<T>>>(
callURL(service),
object: TypeReference<MessageJson<Array<T>>>() {
})
val error = messageJson.error
if (error != null) throw AccountsAPIException(error.errorCode, error.description)
return messageJson.data
} catch (e: Exception) {
throw AccountsAPIException(e)
}
}
One last thing: note that in all my examples I used property getters (get() = ...) as in your original code. However, I strongly suspect that you do NOT want to use a getter. Getters will be called every time someone accesses your property, which in this case means that every time someone reads the positions property you'll be calling constructServiceURL and making the service call, etc. If you want that code to only happen once then you should just call getArrayOf() once and assign the result to your property:
val positions: Array<PositionJson>? = getArrayOf()
// this syntax would also work:
val positions = getArrayOf<PositionJson>()