jackson-module-kotlin
jackson-module-kotlin copied to clipboard
Support for inline classes
Kotlin 1.3 has experimental inline classes.
When wrapping a primitive type, an inline class instance is just compiled to the primitive type, making the code very efficient, whie still keeping it typesafe and preventing for example to sum Watts with Volts.
When the inline class instance is nullable, a wrapper class is used (just like java.lang.Integer is used to represent a nullable Int).
Unfortunately, inline classes don't follow the same rules as standard wrapper classes. Example:
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
inline class Watt(val value: Long)
data class Foo(val nonNullable: Watt, val nullable: Watt?, val otherNullable: Watt?)
data class Bar(val nonNullable: Long, val nullable: Long?, val otherNullable: Long?)
fun main() {
val foo = Foo(Watt(1000), Watt(2000), null)
val bar = Bar(1000, 2000, null)
val objectMapper = ObjectMapper().registerModule(KotlinModule())
println("With Watt: ${objectMapper.writeValueAsString(foo)}")
println("With Long: ${objectMapper.writeValueAsString(bar)}")
}
The output of this program is:
With Watt: {"nonNullable":1000,"nullable":{"value":2000},"otherNullable":null}
With Long: {"nonNullable":1000,"nullable":2000,"otherNullable":null}
It would be really nice if the first output was identical to the second one, and of course if unmarshalling worked too.
I use this workaround to read and write inline classes.
The box-impl
method boxes the value, so it can be used as JsonCreator.
https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#inline-classes-abi-jvm
inline class Watt(@JsonValue val value: Long)
@Test
fun `Read and write inline classes with Jackson`() {
val mapper = ObjectMapper().registerModule(KotlinModule()).registerModule(InlineModule)
val watt = Watt(1234)
val json = mapper.writeValueAsString(watt)
assertEquals("1234", json)
val result = mapper.readValue<Watt>(json)
assertEquals(watt, result)
}
object InlineModule : SimpleModule("Inline") {
override fun setupModule(context: SetupContext) {
super.setupModule(context)
context.appendAnnotationIntrospector(KotlinNamesAnnotationIntrospector)
}
object InlineAnnotationIntrospector : NopAnnotationIntrospector() {
override fun findCreatorAnnotation(config: MapperConfig<*>, a: Annotated): JsonCreator.Mode? {
if (a is AnnotatedMethod && a.name == "box-impl") {
return JsonCreator.Mode.DEFAULT
}
return null
}
}
}
@TjeuKayim, is serialization working with inline classes now?
I tried building the latest master branch, however the build fails.
Instead I used 7bfb771be392073d6580f6a5102c035a4bcfaae9. I ran these unit-tests, and they all failed.
So inline classes are still not working in the latest release.
Please do not use master
as that is for 3.0.0 (and still has couple of issues) -- code for 2.10 is in 2.10
branch (for patches to 2.10), and 2.11
is for developing next 2.x minor version.
This would have to be studied to see how an inline class would be detected safely and without fail, I'm not sure if just the naming convention of the static constructor is clear enough. Maybe if detecting is Kotlin class, plus that naming convention, and a pattern of other methods maybe.
I think this would be a pretty messy fix, but is worth looking into. @TjeuKayim did you abandon that PR completely?
Unfortunately, I can't spend more time on this, and currently I'm not using Kotlin for any project. There has to be changed a lot: https://github.com/TjeuKayim/jackson-module-kotlin/compare/master...TjeuKayim:class-descriptor-reflection. I just found out that reflection functionality has improved since Kotlin 1.3.20 (https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#reflection).
This would have to be studied to see how an inline class would be detected safely @apatrida
I used kotlin.reflect.jvm.internal.impl.descriptorsClassDescriptor.isInline
, but this is an internal API.
Has there been any updates to this? Or workaround code samples? @TjeuKayim, do you have any compartmentalized samples?
No, I don't have any updates. The last few months I have not used Kotlin. For a workaround I refer to my comment above https://github.com/FasterXML/jackson-module-kotlin/issues/199#issuecomment-450650440.
@TjeuKayim thanks. The greater issue i have found is: https://github.com/FasterXML/jackson-module-kotlin/issues/187. Once using the inline within a class, then it seems to fall apart on deserialization.
Found a decent workaround today. Use @JsonDeserialize(builder = BuilderForYourRealClass::class)
Where BuilderForYourRealClass
has "with" functions, one for each argument. The key is that you need a "with" method for the wrapped class. For example if I have an inline class MyInlineClass
that simply wraps a String
then the builder needs a function that accepts a String
and creates an instance of MyInlineClass
in the build
function. Like this:
@JsonDeserialize(builder = MyClassThatUsesAnInlineClassBuilder::class)
data class MyClassThatUsesAnInlineClass(val foo: String, val bar: MyInlineClass)
class MyClassThatUsesAnInlineClassBuilder {
lateinit var foo: String
lateinit var bar: String
fun withFoo(foo: String) = apply { this.foo = foo }
fun withBar(bar: String) = apply { this.bar = bar }
fun build() = MyClassThatUsesAnInlineClass(foo, MyInlineClass(bar))
}
Another possible way would be to use JsonCreator:
inline class InlineType(val value: String)
data class ContainsInlineType(val inlineType: InlineType) {
companion object {
@JsonCreator
@JvmStatic
fun create(inlineType: String) = ContainsInlineType(InlineType(inlineType))
}
}
@TjeuKayim Info: Your InlineAnnotationIntrospector Workaround does not seem to work with Kotlin 1.5. "box-impl" is now synthetic and jackson does not call the Introspector for it...
JsonCreators or Builder would not be a workaround for me either because they have to be added to every class that uses the inline class... This clutters all DTOs...
@k163377 Do you have a plan for value class
deserialization support? With the stabilization of more Kotlin standard library components that utilize value class
es such as kotlin.time.Duration
, this is becoming a pain point for us. Wanted to check with you since you worked on the serialization support. If you don't have plans to work on this soon I may prototype the feature myself for a potential PR.
@Quantum64 As for the partial support, I was going to start with the answer to #514.
As far as I have researched, I think the following two points are relatively easy to achieve.
- deserialize
value class
- deserialize with a factory function (with
JsonCreator
) that includesvalue class
in arguments.
However, there are still some unresolved issues regarding the deserialization of constructor
containing value class
.
Also, support for JsonDeserialize
annotations has not yet been considered, but I feel that there may be some issues that are difficult to resolve.
p.s.
As a personal request, I would like to merge #512 and subsequent refactors first to avoid conflicts and to simplify changes for value class
support.
@k163377 Looking at #512 now…✅
In the process of checking for support, we found that there was still a problem with KFunction
that takes a nullable value class
as a parameter.
It looks like it will be difficult to support value class
until this issue is resolved.
https://youtrack.jetbrains.com/issue/KT-50685
I was also concerned about the fact that this problem has been persisting since Kotlin 1.4.x
.
I fear that if nothing is done, this problem will not be solved during Kotlin 1.6.x
(jackson-module-kotlin 2.14.x
).
I know it is difficult, but I will also check if I can solve the problem on the Kotlin
side.
As for the problem I mentioned before that constructor
that takes value class
as an argument cannot be called, it seems to be fixed by doing the following branch.
https://github.com/k163377/jackson-module-kotlin/tree/github-413/get_constructor_backup
However, I am afraid that incorporating this fix will cause confusion because it will result in a situation where the deserialization of the value class
will work halfway (there are many parts that do not work).
Personally, I think it is better to incorporate this change after doing all of the following (I also hope that the aforementioned kotlin-reflect
problem will be solved while making the changes).
- Merge #538
- Merge PRs that follow #538
- Add a deserializer for
value class
- Modify argument reading of
KVI
to match the case where the argument is avalue class
@dinomite What do you think about the above revision policy? As for myself, I am willing to incorporate this change in my policy that "the more some of it works, the better". I would appreciate your advice.
Sharing the status of the kotlin-reflect
fix.
The following problems with calls to Functions that containing value class
in arguments have been fixed.
- "KotlinReflectionInternalError" when using `callBy` on constructor that has inline class parameters : KT-27598
- IllegalArgumentException when reflectively accessing nullable property of inline class type : KT-31141
However, these fixes are scheduled for release after Kotlin 1.7
, and the jackson-module-kotlin
fixes will come even later.
Finally, a huge thanks to @udalov for his great support of these fixes.
Any news on this topic? Inline classes are a very appealing feature to me, but not being able to use them in Jackson is a big deal...
Unfortunately there the maintainers of this module appear to be currently busy with other projects, help wanted and so on. :-/
You may want to reach out on jackson-dev
mailing list to see if someone from the dev community might step up to look into issues... but this is a challenge with OSS. So many users and -- sometimes -- so little availability by developers.
I would try to help but unfortunately am not very knowledgeable about Kotlin runtime or the module itself.
Inline (value class
) classes have become a major language feature also supporting generics thus it'd be great if Jackson would support them instead of throwing an exception:
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot find a (Map) Key deserializer for type [simple type, class at.wrwks.bef.faelligkeit.domain.aufgabe.AufgabeId]
@andrashatvani PRs welcome.
Is it the same as https://github.com/FasterXML/jackson-module-kotlin/issues/413, but the latest version of Kotlin doesn't support inline class
anymore
I would like to try and take a stab at this, but I think I need a bit of guidance. @k163377 you mentioned
there are many parts [in the deserialization of the
value class
] that do not work
Do we have tests or examples that could give some coverage of those cases? Also those PRs to be merged before make it harder for a first time contributor to understand the scope of changes, would those be a requirement or nice to haves?
kotlin-reflect 1.7
is almost mandatory to support value class
deserialization.
On the other hand, the current jackson-module-kotlin
policy does not allow upgrading to kotlin 1.7
until kotlin 1.6
is deprecated.
This means that you will have to wait a few more years.
Below is a summary of as much as I can remember of the work that needs to be done and the patterns that need to be covered, but I'm not sure if I've covered it all.
Please note that each task requires knowledge of how the value class
is represented in Java
.
Required work
- https://github.com/k163377/jackson-module-kotlin/commit/ca2120b3576c47aecec1d38347d6823abd2d4268
- implement deserialization process for
value class
(probably by adding it toKotlinDeserializers
) - modify
KotlinValueInstantiator
to properly deserialize thevalue class
argument
Note that ideally, Jackson
annotations should also be handled properly.
Patterns that need to be covered
All combinations of the following patterns must be covered.
If the value of value class
is...
- primitive type
- non-null object type
- nullable object type
If the parameter on the Creator
is...
- non-null
- nullable
Also note that ideally, options such as nullToEmptyCollection
, nullIsSameAsDefault
, and strictNullChecks
should be supported.
I am very busy these days and have no time to work on this problem.
If I had the time and money, I would like to work on a project to rewrite jackson-module-kotlin
to my liking...
I have achieved partial support for deserialization by functions containing value class
in an experimental project I am creating.
It is assumed that deserialization of the value class
will work, at least if it was registered a Deserializer
with ObjectMapper
.
https://github.com/ProjectMapK/jackson-module-kogera/pull/40
Snapshots are available from JitPack
.
https://jitpack.io/#ProjectMapK/jackson-module-kogera/8dc5c4abda
Please refer to the following for installation instructions.
https://github.com/ProjectMapK/jackson-module-kogera#installation
However, this project does not use kotlin-reflect
, so it is not possible to apply it directly to jackson-module-kotlin
.
Also, there are some known issues as follows
- The
JsonDeserialize
annotation does not work -
JsonCreator
annotation given invalue class
does not work
I would appreciate a star to keep me motivated. https://github.com/ProjectMapK/jackson-module-kogera
I noticed that there is no explicit specification (at least not that I am aware of) for handling value class
.
In jackson-module-kogera, the basic specification is to treat value class
like a value, as follows.
@JvmInline
value class Value(val value: Int)
val mapper = jacksonObjectMapper()
val json = mapper.writeValueAsString(Value(1)) // -> 1
val value = mapper.readValue<Value>(json) // -> Value(value=1)
The reason for this specification is as follows.
- It matches the current specification of
jackson-module-kotlin
- It also matches the specification of
kotlinx-serialization
- This specification seems natural for the
value class
use case
Also, this specification seemed the easiest to implement due to the constraints of handling value class
in Jackson
.
I plan to use the same specification when support value class
in jackson-module-kotlin
in the future.
I would appreciate any comments or suggestions you may have.
I would especially like to hear from @dinomite .
Issues related to deserialization support for value class
related content will be summarized in #650.
This issue will not be closed for a while as it includes discussion of serialization.