quarkus-operator-sdk icon indicating copy to clipboard operation
quarkus-operator-sdk copied to clipboard

Problem with Injection of ObjectMapper

Open LucaMertens opened this issue 3 years ago • 5 comments

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"}

LucaMertens avatar Sep 02 '22 07:09 LucaMertens

Maybe you can add @RegisterForReflection on your class? like:

@RegisterForReflection 
public class TestResourceSpec 

cdmikechen avatar Sep 13 '22 00:09 cdmikechen

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 😓)

metacosm avatar Sep 14 '22 16:09 metacosm

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 :)

LucaMertens avatar Sep 14 '22 20:09 LucaMertens

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 StartupEvent listener 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.

LucaMertens avatar Oct 09 '22 13:10 LucaMertens

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?

metacosm avatar Oct 10 '22 08:10 metacosm

Closing this as I believe this is fixed. Please re-open if there's still an issue.

metacosm avatar Jan 06 '23 16:01 metacosm

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()
    }
}

LucaMertens avatar Apr 22 '23 15:04 LucaMertens