quarkus-operator-sdk
quarkus-operator-sdk copied to clipboard
Problem with Injection of ObjectMapper
Bug Report
What did you do?
I recently tried to use the Jackson module for Kotlin with the JOSDK.
The module provides an ObjectMapper that can be used to serialize and deserialize Kotlin-specific classes (e.g. data classes).
To inject the ObjectMapper, I used this code snippet from the Quarkus docs.
I've included a minimal reproducer for more details.
What did you expect to see?
The Kotlin object mapper is used to deserialize CRs into data classes
What did you see instead? Under which circumstances?
After applying the CR when the operator is already started, the exception looks like this:
IllegalArgumentException: Failed to deserialize WatchEvent at io.fabric8.kubernetes.client.dsl.internal.AbstractWatchManager.contextAwareWatchEventDeserializer(AbstractWatchManager.java:253)
However, the root cause seems to be:
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `TestResourceSpec` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
How to run the minimal reproducer
git clone https://github.com/LucaMertens/kotlin-object-mapper-minimal-reproducer
cd ./kotlin-object-mapper-minimal-reproducer
./gradlew quarkusDev
# Apply an instance of the CR
kubectl apply -f src/main/resources/sample-resource.yaml
Environment
Kubernetes cluster type: Minikube with Docker backend
io.quarkiverse.operatorsdk:quarkus-operator-sdk:4.0.0 (See build.gradle for more details)
$ java -version
openjdk version "17.0.3" 2022-04-19
OpenJDK Runtime Environment Temurin-17.0.3+7 (build 17.0.3+7)
OpenJDK 64-Bit Server VM Temurin-17.0.3+7 (build 17.0.3+7, mixed mode, sharing)
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"25", GitVersion:"v1.25.0", GitCommit:"a866cbe2e5bbaa01cfd5e969aa3e033f3282a8a2", GitTreeState:"clean", BuildDate:"2022-08-23T17:44:59Z", GoVersion:"go1.19", Compiler:"gc", Platform:"linux/amd64"}
Kustomize Version: v4.5.7
Server Version: version.Info{Major:"1", Minor:"24", GitVersion:"v1.24.3", GitCommit:"aef86a93758dc3cb2c658dd9657ab4ad4afc21cb", GitTreeState:"clean", BuildDate:"2022-07-13T14:23:26Z", GoVersion:"go1.18.3", Compiler:"gc", Platform:"linux/amd64"}
Maybe you can add @RegisterForReflection on your class? like:
@RegisterForReflection
public class TestResourceSpec
There are some things we need to clean up around this and they will be addressed in a future release. Right now, the proper way to register the Kotlin module would be to add a StartupEvent listener to your operator so that you can register the module on the configured ObjectMapper instance:
@Singleton
class ObjectMapperCustomizerStartListener {
@Inject
lateinit var configurationService: ConfigurationService
fun onStart(@Observes event: StartupEvent?) {
configurationService.objectMapper.registerKotlinModule()
}
}
(or something similar, I'm not a Kotlin expert 😓)
Hey @cdmikechen @metacosm, thank you for your suggestions! I won't be able to try them out in the next few days, but I'll get back to you when I have done so :)
Hey, thanks again for the help!
Maybe you can add
@RegisterForReflection
I tried this but it didn't seem to change anything. The error also occurs in non-native builds, so I'm guessing that Reflections were not the issue here :)
Right now, the proper way to register the Kotlin module would be to add a
StartupEventlistener to your operator
I added this to the minimal example. The error persists, but it doesn't seem to be an issue with the Quarkus Plugin:
The method AbstractWatchManager.contextAwareWatchEventDeserializer
uses an ObjectMapper which is created without the user-injected mofifications.
I'm not sure if the Serialization.OBJECT_MAPPER is modified from some other place, but it seems to cause the error.
Complete stacktrace
Invalid event type: java.lang.IllegalArgumentException: Failed to deserialize WatchEvent
at io.fabric8.kubernetes.client.dsl.internal.AbstractWatchManager.contextAwareWatchEventDeserializer(AbstractWatchManager.java:253)
at io.fabric8.kubernetes.client.dsl.internal.AbstractWatchManager.readWatchEvent(AbstractWatchManager.java:259)
at io.fabric8.kubernetes.client.dsl.internal.AbstractWatchManager.onMessage(AbstractWatchManager.java:284)
at io.fabric8.kubernetes.client.dsl.internal.WatcherWebSocketListener.onMessage(WatcherWebSocketListener.java:68)
at io.fabric8.kubernetes.client.okhttp.OkHttpWebSocketImpl$BuilderImpl$1.onMessage(OkHttpWebSocketImpl.java:97)
at okhttp3.internal.ws.RealWebSocket.onReadMessage(RealWebSocket.java:322)
at okhttp3.internal.ws.WebSocketReader.readMessageFrame(WebSocketReader.java:219)
at okhttp3.internal.ws.WebSocketReader.processNextFrame(WebSocketReader.java:105)
at okhttp3.internal.ws.RealWebSocket.loopReader(RealWebSocket.java:273)
at okhttp3.internal.ws.RealWebSocket$1.onResponse(RealWebSocket.java:209)
at okhttp3.RealCall$AsyncCall.execute(RealCall.java:174)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `TestResourceSpec` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: TestResource["spec"])
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1728)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1353)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1415)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:351)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:184)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
at io.fabric8.kubernetes.client.utils.serialization.SettableBeanPropertyDelegate.deserializeAndSet(SettableBeanPropertyDelegate.java:131)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:313)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:176)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4650)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2831)
at com.fasterxml.jackson.databind.ObjectMapper.treeToValue(ObjectMapper.java:3295)
at io.fabric8.kubernetes.client.dsl.internal.AbstractWatchManager.contextAwareWatchEventDeserializer(AbstractWatchManager.java:248)
... 14 more
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `TestResourceSpec` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
There is a workaround to use data classes without the Kotlin-Plugin: Annotating each field with @JsonProperty("fieldName"). In the meantime, I think this is a solid alternative.
I've added support for further customisation of the Fabric8 ObjectMapper in the latest release: you should be able to customize the ObjectMapper that the Fabric8 client relies on by providing an ObjectMapperCustomizer implementation, qualified with the @KubernetesClientSerializationCustomizer qualifier. Did you try this?
Closing this as I believe this is fixed. Please re-open if there's still an issue.
Hey, sorry for the very late reply, I sort of forgot the issue. I just wanted to thank you for adding the feature! I added the following class and it worked like a charm:
@ApplicationScoped
@KubernetesClientSerializationCustomizer
class JacksonKotlinModuleLoader : ObjectMapperCustomizer {
override fun customize(objectMapper: ObjectMapper) {
objectMapper.registerKotlinModule()
}
}