apache / apache/grails-core

Incomplete Jackson 2 classpath breaks Grails 8 startup with NoClassDefFoundError in DefaultHttpMessageConverters

Open
#16,124 3 comments 0 reactions 0 assignees View on GitHub
relates-to: external-plugin
Dominant language
Groovy
Stars
2.9k
Forks
975
Avg merge
1d 22h
Merged PRs (30d)
92

Description

### Summary

A Grails 8 web application fails to start with `NoClassDefFoundError: com/fasterxml/jackson/databind/exc/InvalidDefinitionException` as soon as any dependency puts **Jackson 2 `jackson-core` / `jackson-annotations` on the runtime classpath without Jackson 2 `jackson-databind`**.

Grails 8 ships Jackson 3 for databind (`tools.jackson`), but the managed platform still supplies Jackson 2 `jackson-core`, `jackson-annotations`, and the Jackson 2 dataformat artifacts. When a plugin or library pulls in Jackson 2 core, the resulting classpath has "half of Jackson 2": Spring Framework 7's `DefaultHttpMessageConverters.detectMessageConverters()` concludes Jackson 2 is available and then hard-references a `jackson-databind` 2.x class that is not present.

The failure happens while creating `OrderedFormContentFilter`, so **Tomcat never starts and the whole application is dead**, regardless of whether the app itself uses Jackson at all.

Found while testing released Grails 7 plugins against `8.0.0-M5`.

### Grails Version

8.0.0-M5

### Java / Groovy Version

Java 21.0.11 (Corretto), Groovy 5.0.8, Spring Boot 4.1.0, Spring Framework 7.0.8

### Steps to Reproduce

1. Generate a stock web app with the published 8.0.0-M5 distribution:

```
grails create-app elasticsearch --profile=web
```

2. Add a single dependency that transitively brings Jackson 2 core. Any such dependency reproduces it; the one used here is a released Grails 7 plugin:

```groovy
implementation "org.grails.plugins:grails-elasticsearch:5.1.0"
```

3. Run `gradlew test integrationTest bootJar`, or simply boot the app.

### Actual Behaviour

```
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate
[org.springframework.boot.servlet.filter.OrderedFormContentFilter]:
Factory method 'formContentFilter' threw exception with message:
com/fasterxml/jackson/databind/exc/InvalidDefinitionException

Caused by: java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/exc/InvalidDefinitionException
at org.springframework.http.converter.DefaultHttpMessageConverters$DefaultBuilder.detectMessageConverters(DefaultHttpMessageConverters.java:334)
at org.springframework.http.converter.DefaultHttpMessageConverters$DefaultClientBuilder.build(DefaultHttpMessageConverters.java:468)
at org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter.(AllEncompassingFormHttpMessageConverter.java:42)
at org.springframework.web.filter.FormContentFilter.(FormContentFilter.java:61)
at org.springframework.boot.servlet.filter.OrderedFormContentFilter.(OrderedFormContentFilter.java:29)
at org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration.formContentFilter(WebMvcAutoConfiguration.java:189)

Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.exc.InvalidDefinitionException
```

Wrapped in `WebServerException` from `TomcatWebServer.initialize`, so the context never refreshes.

### Root cause evidence

Contents of `BOOT-INF/lib` in the built `bootJar` - note Jackson 2 core and annotations are present, Jackson 3 databind is present, and **Jackson 2 databind is missing**:

```
BOOT-INF/lib/jackson-core-2.21.5.jar
BOOT-INF/lib/jackson-annotations-2.21.jar
BOOT-INF/lib/jackson-dataformat-cbor-2.21.4.jar
BOOT-INF/lib/jackson-dataformat-smile-2.21.4.jar
BOOT-INF/lib/jackson-dataformat-yaml-2.21.4.jar
BOOT-INF/lib/jackson-databind-3.1.5.jar
BOOT-INF/lib/jackson-core-3.1.5.jar
BOOT-INF/lib/spring-boot-jackson-4.1.0.jar
```

`dependencyInsight` shows the Jackson 2 core arriving transitively and being upgraded by the managed platform:

```
com.fasterxml.jackson.core:jackson-core:2.14.2 -> 2.21.5
\--- org.elasticsearch:elasticsearch-x-content:7.17.29
\--- org.elasticsearch:elasticsearch:7.17.29
\--- org.grails.plugins:grails-elasticsearch:5.1.0

com.fasterxml.jackson.core:jackson-core:2.21.4 -> 2.21.5
\--- org.springframework.boot:spring-boot-dependencies:4.1.0
```

### Confirmation of the diagnosis

Adding the missing Jackson 2 databind to the same app:

```groovy
runtimeOnly "com.fasterxml.jackson.core:jackson-databind:2.21.5"
```

makes the startup crash disappear entirely. The app then boots, the plugin wires up, and the only remaining failure is the plugin's own `ConnectException` reaching an Elasticsearch server - an unrelated, expected environmental error:

```
Caused by: org.elasticsearch.ElasticsearchException at RestHighLevelClient.java:2695
Caused by: java.util.concurrent.ExecutionException at BaseFuture.java:257
Caused by: java.net.ConnectException at Net.java:694
```

So the startup failure is purely the incomplete Jackson 2 classpath, not the plugin's behaviour.

### Expected Behaviour

Either the managed platform keeps Jackson 2 internally consistent (if Jackson 2 `jackson-core` / `jackson-annotations` are managed and can reach the runtime classpath, Jackson 2 `jackson-databind` should be managed and present too), or Jackson 2 artifacts should not be able to reach the application runtime classpath at all, so Spring's Jackson 2 detection never triggers.

Right now a user gets a hard, non-obvious startup crash in an unrelated filter, with nothing pointing at the actual cause.

### Impact

This is not specific to one plugin. Any Grails 7 plugin or third-party library that brings Jackson 2 core without databind - a very common shape, since many libraries depend on `jackson-core` or a Jackson 2 dataformat only - will take down a Grails 8 application at startup. The error message names neither Grails nor the offending dependency.

### Suggested resolution

Options for discussion:

1. Manage `com.fasterxml.jackson.core:jackson-databind` (2.x) in the Grails platform alongside the Jackson 2 `jackson-core` / `jackson-annotations` / dataformat entries so the Jackson 2 classpath is never half-present.
2. Alternatively, keep Jackson 2 fully off the application runtime classpath and let Jackson 3 be the only Jackson present.
3. At minimum, document this failure mode and the `runtimeOnly "com.fasterxml.jackson.core:jackson-databind:<2.x>"` workaround in the Grails 8 upgrade guide's Jackson section, since the stack trace gives no hint.

### Notes

Part of a compatibility sweep of released Grails 7 plugins against 8.0.0-M5. Related plugin-compatibility issues found in the same sweep: #16122 and #16123. Existing compatibility-shim precedents: #16011 and #16101.

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the failure with the grails-elasticsearch dependency, then inspect dependencyInsight and the BOOT-INF/lib contents from bootJar. Read the DefaultHttpMessageConverters.detectMessageConverters stack entry and compare the Jackson 2 and Jackson 3 artifacts. Done means the application starts without NoClassDefFoundError while preserving the intended Jackson dependency behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
groovy, spring-boot
Domain
backend, build-system
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.