As of writing the most current version of Moshi is 1.14.0
and does not support serialization of classes with the
value
modifier.
There is an open issue around this topic currently.
This also indirectly causes an issue for unsigned integer due to the fact that:
Unsigned numbers are implemented as inline classes with the single storage property of the corresponding signed counterpart type of the same width. Nevertheless, changing type from unsigned type to signed counterpart (and vice versa) is a binary incompatible change.
The expected behavior is the inlining of the single property present in the class, effectively "flattening" the declared property name, such that it's not present in the serialized representation.
If we were to serialize a value class
by running the below code snippet:
@JvmInline
value class Username(val input: String)
val username = Username(input = "amichne")
val jsonString = Moshi.Builder().build()
.adapter<Username>()
.toJson(username)
We would expect the below statement to return true
:
jsonString == "amichne"
Currently, the result is false
, as jsonString
would actually equal:
{
"input": "amichne"
}
Clearly we receive an incorrect JSON representation. The expected result is a string token, but the actual result is an object with a single key containing a value that is the expected string token.
Similarly, if we wanted to deserialize a JSON representation to a value class
instance via the below code snippet:
@JvmInline
value class Username(val input: String)
val jsonStringForUsername = "amichne"
val username = Moshi.Builder().build()
.adapter<Username>()
.fromJson(jsonStringForUsername)
We will receive the below exception:
JsonDataException(message = "Expected BEGIN_OBJECT but was STRING at path $")
Unsigned integers observe the same issue as above, but they have another issue as well.
The compiled bytecode for unsigned integers has a single constructor, which accepts a signed integer. This causes a lookup failure when reflection is trying to fetch the constructor for the type, as it's expecting an unsigned integer.
Value class (ValueClassAdapterFactory
)
- If the type being looked up has the
value
modifier and is not in[ULong, UInt, UShort, UByte]
- Else return
null
- Else return
- Then resolve the constructor for the type, as well as the type of its declared property
- We can guarantee there is only a declared property, per the Kotlin Specification for value classes
- Resolve the Moshi adapter for the type of the declared property
- Create the
ValueClassAdapter
with the resolved constructor and adapter
- Retrieve the declared property reflectively
- Serialize the value of the retrieved property using
<ValueClassAdapter>.adapter
- Write the result of serialization to the
JsonWriter
- Read the next JSON value from the
JsonReader
- Deserialize the JSON content to the type of the declared property using
<ValueClassAdapter>.adapter
- Invoke
<ValueClassAdapter>.constructor
with the declared property parameter and return the result
Unsigned integers (UnsignedAdapterFactory
)
- If the type (
T
) being looked up is in[ULong, UInt, UShort, UByte]
- Else return
null
- Else return
- Then return the
UnsignedTypeAdapter<T>
with the type mapperULong.() -> T
- Convert the unsigned value to a string representation
- Write the string representation without any additional encoding
- This is required to avoid the scenario where a unsigned number with a most significant bit being written as a negative value
- Peek the next token in the
JsonReader
- If it is
NUMBER
- Else if it's
NULL
read the next null viareader.nextNull()
- Else throw an exception, as we know it must be an illegal value
- Else if it's
- Then read it into a string literal
- This is required to avoid the situation where we would read an unsigned long larger than
Long.MAX_VALUE
which would result in a error despite valid data being deserialized
- This is required to avoid the situation where we would read an unsigned long larger than
- Convert the string to an unsigned long
- Right now we're enforcing that the original value can't be larger than the unsigned types MAX_VALUE, but I don't know if this is the right choice
- Convert the unsigned long to the requested unsigned type
The code in ValueClassAdapterFactory
solves the issue for user-created value class
declarations.
The code in UnsignedAdapterFactory
solves the specific complications introduced in the case of unsigned integers.
JSON Literal:
"exampleValue"
Kotlin Object:
@JvmInline
value class JvmInlineString(val value: String)
JvmInlineString(value = exampleValue)
Base Moshi Deserialization Result:
Expected BEGIN_OBJECT but was STRING at path $
Base Moshi Serialization Result:
{
"value": "exampleValue"
}
Updated Moshi Deserialization Result:
JvmInlineString(value=exampleValue)
Updated Moshi Serialization Result:
"exampleValue"
JSON Literal:
{
"uLong": 9223372039002259454
}
Kotlin Object:
DataClassWithULong(
uLong = 9223372039002259454u
)
Base Moshi Deserialization Result:
Platform class kotlin.ULong requires explicit JsonAdapter to be registered for class kotlin.ULong
Base Moshi Serialization Result:
Platform class kotlin.ULong requires explicit JsonAdapter to be registered for class kotlin.ULong
Updated Moshi Deserialization Result:
DataClassWithULong(
uLong = 9223372039002259454u
)
Updated Moshi Serialization Result:
{
"uLong": 9223372039002259454
}
Scenarios
JvmInlineString
JSON Literal:
"exampleValue"
Kotlin Object:
JvmInlineString(value=exampleValue)
Base Moshi Deserialization Result:
Expected BEGIN_OBJECT but was STRING at path $
Base Moshi Serialization Result:
{"value":"exampleValue"}Updated Moshi Deserialization Result:
JvmInlineString(value=exampleValue)
Updated Moshi Serialization Result:
"exampleValue"
JvmInlineInt
JSON Literal:
10
Kotlin Object:
JvmInlineInt(value=10)
Base Moshi Deserialization Result:
Expected BEGIN_OBJECT but was NUMBER at path $
Base Moshi Serialization Result:
{"value":10}Updated Moshi Deserialization Result:
JvmInlineInt(value=10)
Updated Moshi Serialization Result:
10
JvmInlineDouble
JSON Literal:
0.5
Kotlin Object:
JvmInlineDouble(value=0.5)
Base Moshi Deserialization Result:
Expected BEGIN_OBJECT but was NUMBER at path $
Base Moshi Serialization Result:
{"value":0.5}Updated Moshi Deserialization Result:
JvmInlineDouble(value=0.5)
Updated Moshi Serialization Result:
0.5
JvmInlineComplexClass
JSON Literal:
{"stringValue":"a string","intValue":10}Kotlin Object:
JvmInlineComplexClass(value=ExampleNestedClass(stringValue=a string, intValue=10))
Base Moshi Deserialization Result:
Required value 'value' missing at $
Base Moshi Serialization Result:
{"value":{"stringValue":"a string","intValue":10}}Updated Moshi Deserialization Result:
JvmInlineComplexClass(value=ExampleNestedClass(stringValue=a string, intValue=10))
Updated Moshi Serialization Result:
{"stringValue":"a string","intValue":10}JvmInlineListInt
JSON Literal:
[0,2,99]Kotlin Object:
JvmInlineListInt(list=[0, 2, 99])
Base Moshi Deserialization Result:
Expected BEGIN_OBJECT but was BEGIN_ARRAY at path $
Base Moshi Serialization Result:
{"list":[0,2,99]}Updated Moshi Deserialization Result:
JvmInlineListInt(list=[0, 2, 99])
Updated Moshi Serialization Result:
[0,2,99]JvmInlineMapStringNullableInt
JSON Literal:
{"first":1,"missing":null}Kotlin Object:
JvmInlineMapStringNullableInt(map={first=1, missing=null})
Base Moshi Deserialization Result:
Required value 'map' missing at $
Base Moshi Serialization Result:
{"map":{"first":1}}Updated Moshi Deserialization Result:
JvmInlineMapStringNullableInt(map={first=1, missing=null})
Updated Moshi Serialization Result:
{"first":1}JvmInlineMapComplexClass
JSON Literal:
{"key":{"stringValue":"a string","intValue":10}}Kotlin Object:
JvmInlineMapComplexClass(parameterizedValue={key=JvmInlineComplexClass(value=ExampleNestedClass(stringValue=a string, intValue=10))})
Base Moshi Deserialization Result:
Required value 'parameterizedValue' missing at $
Base Moshi Serialization Result:
{"parameterizedValue":{"key":{"value":{"stringValue":"a string","intValue":10}}}}Updated Moshi Deserialization Result:
JvmInlineMapComplexClass(parameterizedValue={key=JvmInlineComplexClass(value=ExampleNestedClass(stringValue=a string, intValue=10))})
Updated Moshi Serialization Result:
{"key":{"stringValue":"a string","intValue":10}}JvmInlineString
JSON Literal:
"baseAppended"
Kotlin Object:
JvmInlineString(value=baseAppended)
Base Moshi Deserialization Result:
Expected BEGIN_OBJECT but was STRING at path $
Base Moshi Serialization Result:
{"value":"baseAppended"}Updated Moshi Deserialization Result:
JvmInlineString(value=baseAppended)
Updated Moshi Serialization Result:
"baseAppended"
JvmInlineNullableString
JSON Literal:
"notNull"
Kotlin Object:
JvmInlineNullableString(value=notNull)
Base Moshi Deserialization Result:
Expected BEGIN_OBJECT but was STRING at path $
Base Moshi Serialization Result:
{"value":"notNull"}Updated Moshi Deserialization Result:
JvmInlineNullableString(value=notNull)
Updated Moshi Serialization Result:
"notNull"
JvmInlineNullableString
JSON Literal:
null
Kotlin Object:
JvmInlineNullableString(value=null)
Base Moshi Deserialization Result:
null
Base Moshi Serialization Result:
{}Updated Moshi Deserialization Result:
JvmInlineNullableString(value=null)
Updated Moshi Serialization Result:
null
JvmInlineComplexClassWithParameterizedField
JSON Literal:
{"strings":["i","have","strings"],"ints":[5,10]}Kotlin Object:
JvmInlineComplexClassWithParameterizedField(value=ExampleNestedClassWithParameterizedField(strings=[i, have, strings], ints=[5, 10]))
Base Moshi Deserialization Result:
Required value 'value' missing at $
Base Moshi Serialization Result:
{"value":{"strings":["i","have","strings"],"ints":[5,10]}}Updated Moshi Deserialization Result:
JvmInlineComplexClassWithParameterizedField(value=ExampleNestedClassWithParameterizedField(strings=[i, have, strings], ints=[5, 10]))
Updated Moshi Serialization Result:
{"strings":["i","have","strings"],"ints":[5,10]}JvmInlineUInt
JSON Literal:
99
Kotlin Object:
JvmInlineUInt(unsignedValue=99)
Base Moshi Deserialization Result:
Platform class kotlin.UInt requires explicit JsonAdapter to be registered for class kotlin.UInt unsignedValue for class io.amichne.moshi.extension.JvmInlineUInt
Base Moshi Serialization Result:
Platform class kotlin.UInt requires explicit JsonAdapter to be registered for class kotlin.UInt unsignedValue for class io.amichne.moshi.extension.JvmInlineUInt
Updated Moshi Deserialization Result:
JvmInlineUInt(unsignedValue=99)
Updated Moshi Serialization Result:
99
DataClassWithULong
JSON Literal:
{"uLong":9223372039002259454}Kotlin Object:
DataClassWithULong(uLong=9223372039002259454)
Base Moshi Deserialization Result:
Platform class kotlin.ULong requires explicit JsonAdapter to be registered for class kotlin.ULong uLong for class io.amichne.moshi.extension.DataClassWithULong
Base Moshi Serialization Result:
Platform class kotlin.ULong requires explicit JsonAdapter to be registered for class kotlin.ULong uLong for class io.amichne.moshi.extension.DataClassWithULong
Updated Moshi Deserialization Result:
DataClassWithULong(uLong=9223372039002259454)
Updated Moshi Serialization Result:
{"uLong":9223372039002259454}DataClassWithUInt
JSON Literal:
{"uInt":2147516414}Kotlin Object:
DataClassWithUInt(uInt=2147516414)
Base Moshi Deserialization Result:
Platform class kotlin.UInt requires explicit JsonAdapter to be registered for class kotlin.UInt uInt for class io.amichne.moshi.extension.DataClassWithUInt
Base Moshi Serialization Result:
Platform class kotlin.UInt requires explicit JsonAdapter to be registered for class kotlin.UInt uInt for class io.amichne.moshi.extension.DataClassWithUInt
Updated Moshi Deserialization Result:
DataClassWithUInt(uInt=2147516414)
Updated Moshi Serialization Result:
{"uInt":2147516414}DataClassWithUShort
JSON Literal:
{"uShort":32894}Kotlin Object:
DataClassWithUShort(uShort=32894)
Base Moshi Deserialization Result:
Platform class kotlin.UShort requires explicit JsonAdapter to be registered for class kotlin.UShort uShort for class io.amichne.moshi.extension.DataClassWithUShort
Base Moshi Serialization Result:
Platform class kotlin.UShort requires explicit JsonAdapter to be registered for class kotlin.UShort uShort for class io.amichne.moshi.extension.DataClassWithUShort
Updated Moshi Deserialization Result:
DataClassWithUShort(uShort=32894)
Updated Moshi Serialization Result:
{"uShort":32894}DataClassWithUByte
JSON Literal:
{"uByte":137}Kotlin Object:
DataClassWithUByte(uByte=137)
Base Moshi Deserialization Result:
Platform class kotlin.UByte requires explicit JsonAdapter to be registered for class kotlin.UByte uByte for class io.amichne.moshi.extension.DataClassWithUByte
Base Moshi Serialization Result:
Platform class kotlin.UByte requires explicit JsonAdapter to be registered for class kotlin.UByte uByte for class io.amichne.moshi.extension.DataClassWithUByte
Updated Moshi Deserialization Result:
DataClassWithUByte(uByte=137)
Updated Moshi Serialization Result:
{"uByte":137}DataClassWithUIntAndString
JSON Literal:
{"stringValue":"foo","unsignedValue":2147516414}Kotlin Object:
DataClassWithUIntAndString(stringValue=foo, unsignedValue=2147516414)
Base Moshi Deserialization Result:
Platform class kotlin.UInt requires explicit JsonAdapter to be registered for class kotlin.UInt unsignedValue for class io.amichne.moshi.extension.DataClassWithUIntAndString
Base Moshi Serialization Result:
Platform class kotlin.UInt requires explicit JsonAdapter to be registered for class kotlin.UInt unsignedValue for class io.amichne.moshi.extension.DataClassWithUIntAndString
Updated Moshi Deserialization Result:
DataClassWithUIntAndString(stringValue=foo, unsignedValue=2147516414)
Updated Moshi Serialization Result:
{"stringValue":"foo","unsignedValue":2147516414}