WxJava Upgrade Guide
binarywang/WxJava
Plans a WxJava version upgrade, checking the BOM, module dependencies, JDK version and configuration, with phased steps and clear rollback conditions.
Guides moving a Grails application from 7.x to Grails 8 and turns the official upgrade guide into a checklist of breaking changes.
$ npx skills add apache/grails-core --skill grails-8-upgrade -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install apache/grails-core grails-8-upgrade --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/apache/grails-core.git skills-src && mkdir -p .claude/skills && cp -r skills-src/grails-skills/upgrade-guide-8/skills/grails-8-upgrade .claude/skills/grails-8-upgrade && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "grails-8-upgrade" agent skill from https://github.com/apache/grails-core/tree/8.0.x/grails-skills/upgrade-guide-8/skills/grails-8-upgrade into .claude/skills/grails-8-upgrade/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "grails-8-upgrade", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/apache/grails-core/tree/8.0.x/grails-skills/upgrade-guide-8/skills/grails-8-upgradeType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add apache/grails-core --skill grails-8-upgrade -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install apache/grails-core grails-8-upgrade --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/apache/grails-core.git skills-src && mkdir -p .agents/skills && cp -r skills-src/grails-skills/upgrade-guide-8/skills/grails-8-upgrade .agents/skills/grails-8-upgrade && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "grails-8-upgrade" agent skill from https://github.com/apache/grails-core/tree/8.0.x/grails-skills/upgrade-guide-8/skills/grails-8-upgrade into .agents/skills/grails-8-upgrade/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "grails-8-upgrade", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add apache/grails-core --skill grails-8-upgrade -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install apache/grails-core grails-8-upgrade --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/apache/grails-core.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/grails-skills/upgrade-guide-8/skills/grails-8-upgrade .cursor/skills/grails-8-upgrade && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "grails-8-upgrade" agent skill from https://github.com/apache/grails-core/tree/8.0.x/grails-skills/upgrade-guide-8/skills/grails-8-upgrade into .cursor/skills/grails-8-upgrade/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "grails-8-upgrade", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/apache/grails-core.git --path grails-skills/upgrade-guide-8/skills/grails-8-upgrade--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add apache/grails-core --skill grails-8-upgrade -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install apache/grails-core grails-8-upgrade --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/apache/grails-core.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/grails-skills/upgrade-guide-8/skills/grails-8-upgrade .gemini/skills/grails-8-upgrade && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "grails-8-upgrade" agent skill from https://github.com/apache/grails-core/tree/8.0.x/grails-skills/upgrade-guide-8/skills/grails-8-upgrade into .gemini/skills/grails-8-upgrade/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "grails-8-upgrade", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install apache/grails-core grails-8-upgradeInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add apache/grails-core --skill grails-8-upgrade -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/apache/grails-core.git skills-src && mkdir -p .github/skills && cp -r skills-src/grails-skills/upgrade-guide-8/skills/grails-8-upgrade .github/skills/grails-8-upgrade && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "grails-8-upgrade" agent skill from https://github.com/apache/grails-core/tree/8.0.x/grails-skills/upgrade-guide-8/skills/grails-8-upgrade into .github/skills/grails-8-upgrade/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "grails-8-upgrade", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add apache/grails-core --skill grails-8-upgrade -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install apache/grails-core grails-8-upgrade --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/apache/grails-core.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/grails-skills/upgrade-guide-8/skills/grails-8-upgrade .opencode/skills/grails-8-upgrade && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "grails-8-upgrade" agent skill from https://github.com/apache/grails-core/tree/8.0.x/grails-skills/upgrade-guide-8/skills/grails-8-upgrade into .opencode/skills/grails-8-upgrade/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "grails-8-upgrade", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
grails-8-upgradeGuides moving a Grails application from 7.x to Grails 8 and turns the official upgrade guide into a checklist of breaking changes.
The skill treats the published Grails documentation as the source of truth and turns the Grails 8 upgrade guide into a practical migration checklist focused on public application behavior, not framework internals. It flags changes that can break an app: Java 21, Groovy 5 name resolution and static compilation, Spring Boot 4.1, Spring Framework 7, Jackson 3, the Gradle platform, Micronaut, Hibernate, TagLibs, testing, validation and content negotiation.
It is meant for updating an application to Grails 8, reviewing an upgrade branch or pull request, fixing tests, build failures or runtime changes after the move, and deciding whether to stay on Hibernate 5 or opt in to Hibernate 7. Primary sources include the upgrade guide, Spring Boot's migration guide, the Grails BOM reference and the content negotiation section, with the version taken from `grailsVersion` in `gradle.properties`.
7 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 4deae57. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are groovy and yaml).
From the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
grails.apache.orggithub.comAlso links to:
issues.apache.orgFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Grails 8 Upgrade Guide loads about 20k tokens when it runs. Until then it costs about 87 tokens; SKILL.md has 9,454 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from apache/grails-core at commit 4deae57, republished under its Apache-2.0 licence (© apache). 9,454 words, ~20,070 tokens.
.claude/skills/grails-8-upgrade/SKILL.md (or your agent's skills folder).<!--
SPDX-License-Identifier: Apache-2.0
Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0.
-->
Activate this skill when:
Use the published Grails documentation as the source of truth before changing an application. The URLs below use <version>, which stands for the Grails 8 version the application is upgrading to, such as the grailsVersion in its gradle.properties; use snapshot for a snapshot version.
| URL | Use for |
|---|---|
https://grails.apache.org/docs/<version>/guide/upgrading.html#upgrading80x | Main Grails 7 to Grails 8 upgrade guide |
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide | Boot module splits, Jackson, session, Batch, and test infrastructure changes; also review the target 4.1 release notes |
https://grails.apache.org/docs/<version>/guide/introduction.html#whatsNew | Grails 8 feature and platform overview |
https://grails.apache.org/docs/<version>/guide/introduction.html#dependencyUpgrades | Platform dependency baseline |
https://grails.apache.org/docs/<version>/ref/Versions/Grails%20BOM.html | Dependency versions of the Grails BOM, with links to the Hibernate 5, Hibernate 7, and Neo4j BOM variants |
https://grails.apache.org/docs/<version>/guide/upgrading.html#micronaut-integration | Micronaut integration, which points to the Grails Micronaut project's own upgrade documentation |
https://grails.apache.org/docs/<version>/guide/theWebLayer.html#contentNegotiation | MIME defaults and Accept header behavior |
https://grails.apache.org/docs/<version>/grails-data/hibernate7/manual/index.html#upgradeNotes | Hibernate 7 GORM query and tenant-schema notes |
https://grails.apache.org/docs/<version>/grails-data/hibernate7/manual/index.html#databaseMigration | Database migration plugin for Hibernate 7, which uses the same version as Grails |
Do not assume https://grails.apache.org/docs/latest/ points at Grails 8. Check the version printed on the snapshot documentation too: snapshot can track a newer development branch. When testing a local 8.0.x checkout, compare its grails-doc/src/en/guide/upgrading/upgrading80x.adoc and dependencies.gradle with the published guide before applying guidance from a later branch.
Start every Grails 8 upgrade by checking these platform requirements:
grails-micronaut, micronaut-http-client, or other Micronaut features.jakarta.* APIs. Do not reintroduce javax.* packages.runtimeOnly 'org.springframework.boot:spring-boot-properties-migrator' temporarily during the migration, boot once, fix reported configuration properties, then remove it.Use this order so failures are isolated and reversible:
For large applications, batch syntax-only checks across source files with the target Groovy compiler before repeatedly compiling the whole application. A build can stop at the first parser failure even when dozens of files share it. Parsing is a diagnostic step, not a substitute for compilation or tests.
Before compensating for an apparent framework/compiler regression, reproduce the same public behavior on both the old and target stacks with neutral fixtures. Distinguish an intentional migration, a pre-existing assumption exposed by the upgrade, and a pre-release defect. For overload or proxy failures, compare concrete instances with proxies and inspect the compiled call target; an Object fallback does not by itself prove that runtime type matching is broken. Prefer a framework fix for a confirmed regression, and document narrowly scoped application workarounds beside the code.
The following differences were verified against Grails Forge-generated 7.2.4 and 8.0.0-SNAPSHOT web applications, both using Hibernate 5 and Undertow. The Grails 8 baseline selected logback-config to make its optional logging file explicit. These are generated defaults; retain deliberate application customizations where they remain supported.
| Area | Grails 7.2.4 baseline | Grails 8.0.0-SNAPSHOT baseline | Upgrade action |
|---|---|---|---|
| Undertow | spring-boot-starter-undertow | org.apache.grails:grails-undertow | Use the Grails Undertow module and its managed servlet dependencies. |
| Undertow thread library | Explicit runtimeOnly 'org.jboss.threads:jboss-threads:3.9.2' | No direct declaration | Remove the old generated pin and let the new stack resolve it. |
| Layouts | org.apache.grails:grails-layout | org.apache.grails:grails-sitemesh3 | SiteMesh 3 is the new generated default; migrate configuration and custom taglib integrations when adopting it. |
| Jansi | runtimeOnly 'org.fusesource.jansi:jansi' | No application runtime declaration | Remove an inherited dependency together with any custom withJansi=true settings. |
| Logback | Explicit console appender, withJansi=false, Boot encoder defaults, root ERROR | Optional logback-config file includes Boot's defaults.xml and console-appender.xml, root INFO, no Jansi setting | Preserve intentional log levels and custom appenders, but remove obsolete Jansi wrapping. Boot's %clr supplies ANSI color without it. |
| Mockito | No explicit Mockito dependency in the generated web baseline | testImplementation 'org.mockito:mockito-core' | Keep Mockito test-scoped where tests need it. |
| Console | Explicit console 'org.apache.grails:grails-console' | No explicit declaration in the generated baseline | Review separate CLI/tooling requirements; this is not an application runtime dependency. |
| Java compilation | Release 17 | Release 21 | Raise the baseline to at least 21; retaining a supported higher application target is valid. |
Grails 8 no longer applies the io.spring.dependency-management plugin by default.
platform() dependency management with the Grails BOM.gradle.properties and ext['property.version'] still work through the bundled org.apache.grails.gradle.bom-property-overrides plugin.| Grails 7 or earlier Grails 8 property | Grails 8 property |
|---|---|
jackson.version, jackson2.version | jackson-2-bom.version (Jackson 2, com.fasterxml.jackson.*) |
jackson3.version | jackson-bom.version (Jackson 3, tools.jackson.*, the default) |
jackson-bom.version set to a 2.x version (Spring Boot 3's name for Jackson 2) | jackson-2-bom.version; in Grails 8 jackson-bom.version sets Jackson 3, so a 2.x value makes dependency resolution fail |
neo4j-driver.version (grails-neo4j-bom) | neo4j-java-driver.version |
grails { springDependencyManagement = false } with grails { bom = null } for new builds that intentionally opt out.resolutionStrategy.force where a transitive version must be forced.enforcedPlatform("org.apache.grails:grails-bom:$grailsVersion").dependencyInsight on the configuration that actually supplies the failing code. A direct dependency version is not a force: a BOM can select a newer version, and browser-test, runtime, asset-development, and build-plugin configurations can select different versions. Keep application version properties consistent with the verified BOM baseline or deliberate newer overrides; do not infer the loaded jQuery or other library version from gradle.properties alone.For builds with buildSrc or an included build-logic project:
org.apache.grails:grails-gradle-bom for the Gradle plugin classpath. Gradle 9 embeds Groovy 4, while the application BOM manages Groovy 5; do not import the application runtime stack into convention-plugin compilation.org.gradle.util.ConfigureUtil; 1.2.3 works with the Gradle 9 build configuration.publishAllToMavenLocal task also publishes Forge in order. Enable Maven Local separately for plugin management, application dependency resolution, and any included build's repositories.grails-gradle-plugins transitively resolves the Grails publish plugin even when no publishing task runs. A pre-release framework checkout may require ASF staging for that dependency; use a repository content filter and make this local-validation setup explicit.org.apache.grails.data:grails-datamapping-async with org.apache.grails:grails-datamapping-async. Remove an old unversioned org.fusesource.jansi:jansi runtime dependency if it was only inherited from generated Grails 7 build files; it is no longer managed for application runtime.<withJansi>true</withJansi> directives from every profile in custom Logback configuration, including custom ConsoleAppender subclasses. Otherwise Logback still tries to load org.fusesource.jansi.AnsiConsole and warns before falling back to the ordinary stream. Grails 8's generated logging configuration uses Spring Boot's console defaults without Jansi; native ANSI output can still use Boot's %clr converter. Jansi's global stream replacement can also break logging after a DevTools restart (Grails issue #15663).Spring Boot 4 renamed common starters:
| Spring Boot 3 starter | Spring Boot 4 starter |
|---|---|
spring-boot-starter-web | spring-boot-starter-webmvc |
spring-boot-starter-aop | spring-boot-starter-aspectj |
spring-boot-starter-web-services | spring-boot-starter-webservices |
spring-boot-starter-oauth2-authorization-server | spring-boot-starter-security-oauth2-authorization-server |
spring-boot-starter-oauth2-client | spring-boot-starter-security-oauth2-client |
spring-boot-starter-oauth2-resource-server | spring-boot-starter-security-oauth2-resource-server |
For WAR deployment to an external servlet container, replace providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' with providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat-runtime'.
Spring Boot 4 dropped spring-boot-starter-undertow; Grails 8 provides Undertow through the Grails Undertow plugin instead. For an application that runs on Undertow, make the build match what Grails Forge generates for --servlet=undertow:
spring-boot-starter-undertow with implementation 'org.apache.grails:grails-undertow', without a version; the Grails BOM manages it.io.undertow:undertow-servlet and io.undertow:undertow-websockets-jsr dependencies from the old stack. Servlet 6.1 support uses Undertow core 2.4.x with servlet/websocket artifacts in io.undertow.ee; let the Grails Undertow plugin and BOM supply the compatible set.spring-boot-starter-tomcat or spring-boot-tomcat; those are what Forge adds for Tomcat applications instead.server.undertow.* properties; they continue to work. server.undertow.max-http-post-size now defaults to 2MB, Undertow's hardened default; set it to -1 only if the application relied on the previous unlimited request size.Spring Retry is no longer managed by Spring Boot. If the application directly uses its RetryTemplate, @Retryable, @EnableRetry, or @Recover, declare org.springframework.retry:spring-retry directly. The Grails 8 BOM manages its version; otherwise supply an explicit version or migrate to Spring Framework's retry API. A transitive dependency is not a declaration of the application's own use.
spring-boot-session, Spring Integration needs spring-boot-integration, and JSON mapping needs spring-boot-jackson (or their appropriate starters). Check actual beans and behavior after adding a module; successful compilation cannot detect missing auto-configuration.JSESSIONID instead of SESSION; decoding a native cookie as a Base64 session ID can send binary/NUL bytes to a JDBC lookup. A database error on the session-ID query parameter is not proof of corrupt stored attributes. Test a native cookie, a valid Spring Session cookie, and a cookie-free first request.spring.session.servlet.filter-order. Audit dispatcher types too: an ERROR dispatch can re-enter Spring Session despite an API filter marking the request as already filtered. Where the application deliberately excludes error dispatches, configure spring.session.servlet.filter-dispatcher-types=REQUEST,ASYNC and test real API error responses. A comma-separated scalar worked with the checked Grails 8 configuration; a Groovy list of strings bound without converting its elements to Boot's dispatcher enum.Check for direct imports or references to Spring Boot auto-configuration classes. Spring Boot 4 split the old monolithic auto-configure module into domain modules, and many classes moved.
Check both the package and the supplying module rather than guessing package renames. FilterRegistrationBean and ServletContextInitializer remain in org.springframework.boot.web.servlet. DataSourceHealthIndicator moves to org.springframework.boot.jdbc.health and needs an explicit spring-boot-jdbc dependency when application code references it. For an optional auto-configuration used only in an exclusion, @EnableAutoConfiguration(excludeName = ['org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration']) avoids adding a module just to reference its class.
Examples:
// Before
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration
// After
import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfigurationIf the application contributes an EnvironmentPostProcessor, update both imports and META-INF/spring.factories keys from org.springframework.boot.env.EnvironmentPostProcessor to org.springframework.boot.EnvironmentPostProcessor.
Other common Spring changes:
HttpStatus.MOVED_TEMPORARILY to HttpStatus.FOUND.HttpStatus.UNPROCESSABLE_ENTITY.value() rather than the HttpStatus object. Keep numeric wire assertions separate from typed client-response status assertions.HandlerAdapter.getLastModified overrides.ThemeSource, Theme, SimpleTheme, and SessionThemeResolver.SecurityProperties.DEFAULT_FILTER_ORDER with -100 only when a direct constant replacement is needed, and prefer fluent HttpSecurity configuration long term.TomcatServletWebServerFactory to the new Spring Boot 4 packages.spring-boot-tomcat explicitly if application code directly references Tomcat classes.ContentCachingRequestWrapper constructor with the request and an explicit cache limit. A limit of 0 preserves the former unlimited behavior; choose a finite limit only with deliberate handling of truncated cached bodies.convertAndSend(Object payload, Map headers). A Groovy call convertAndSend(topic, mapPayload) can select this instead of the destination/payload overload, producing No 'defaultDestination' configured or silently sending the topic string as payload to an unrelated configured default. Cast the map argument explicitly: convertAndSend(topic, (Object) mapPayload). Verify the destination and payload with a real SimpMessagingTemplate and message channel, both with and without a default destination; mocks alone can hide the overload mismatch.value instanceof List<MyType> with value instanceof List (or an unbounded wildcard). The runtime check never verified element types. If static compilation needs the generic type afterwards, keep it in an explicit cast or typed local variable.value as boolean when value can be null: Groovy 5 can throw IllegalArgumentException: null to boolean. Use value != null for presence checks or !!value for Groovy truth, preserving empty collection/string and zero semantics where relevant.map.get(key) / containsKey(key) when testing entries such as metaClass, rather than bean-property introspection. Groovy 5 File/Path truth can test filesystem existence; use path != null when asserting that a path was supplied, and an explicit existence check when that is the contract.new LinkedHashSet<Element>(source) when a shallow copy is intended and a runtime collection's clone() is inaccessible. Do not rely on reflective access to a protected implementation method.StackOverflowError before changing the query. An acronym getter such as getURLValue(LocalDate asOf = LocalDate.now()) also exposes a zero-argument getter and the case-sensitive property URLValue. In the checked Groovy 5 stack, URLValue.lookup() inside that getter resolved the property and re-entered the getter instead of selecting the class; the same plain-Groovy example worked on the Groovy 4 stack. Qualify the class (example.URLValue.lookup()) and comment why the qualification prevents recursion. This is not specific to GORM or Hibernate proxies.static constraints, for example matches: Book.CODE_PATTERN or validator: Book.codeValidator. Under Groovy 5, unqualified names can resolve against the mapping DSL delegate instead of the declaring class and fail during datastore initialization.static constraints as local references; qualifying those as class members introduces a missing-property error. Apply the same ownership check to static helpers called from binding/validation closures: qualify the declaring class when lookup is hitting the wrong owner or delegate.ConfigObject.properties is a read-only bean property in Groovy 5. For datasource or Quartz configuration whose actual key is properties, construct a map and use put('properties', values) or putAll([properties: values]); do not assign delegate.properties in a configuration closure.The following generic example was compared using the Grails 7.2.4 / Groovy 4 and Grails 8 snapshot / Groovy 5.1.3 stacks. It has no GORM domain, database, or proxy:
package example
import java.time.LocalDate
class URLValue {
static URLValue lookup() { new URLValue() }
}
class ReferenceHolder {
URLValue getURLValue(LocalDate asOf = LocalDate.now()) {
// Qualify the class to avoid resolving the URLValue property and recursively calling this getter.
example.URLValue.lookup()
}
}| Receiver expression inside the getter | Groovy 4 stack | Groovy 5.1.3 stack |
|---|---|---|
URLValue.lookup() | Returns a value | Recursive getter / StackOverflowError |
example.URLValue.lookup() | Returns a value | Returns a value |
The return type is a type reference; the receiver of the method call is where the ambiguity occurs. For a domain query, the same diagnosis applies to URLValue.createCriteria(). Qualify only the ambiguous receiver, preserve the query, and verify against the actual compiler version before treating the workaround as permanent.
@Slf4j logger from logger inheritance. A returned closure from a Spring-enhanced @Configuration class failed with MissingPropertyException: log naming the generated CGLIB subclass, despite the declaring class having @Slf4j. Qualifying ClientConfiguration.log inside that closure selects the same logger generated on ClientConfiguration; it does not introduce a superclass logger. Verify the actual proxied bean and callback; a directly constructed configuration instance may not reproduce the failure.@Slf4j field is a separate visibility concern. If subclasses intentionally share it, configure protected visibility on the base logger with @Slf4j(visibilityId = 'logger') and @VisibilityOptions(id = 'logger', value = Visibility.PROTECTED); if a base-class closure should use the base logger, qualify that declaring class. Preserve intentional logger categories rather than adding duplicate logging annotations mechanically. Remove unused logging annotations from interfaces, where their generated private field is invalid.import groovy.util.logging.Slf4j
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Slf4j
@Configuration
class ClientConfiguration {
@Bean
Runnable clientCallback() {
return () -> {
// Use this configuration class's logger rather than looking up log on the CGLIB subclass.
ClientConfiguration.log.debug('Configuring client')
}
}
}These are targeted receiver/visibility fixes, not a general requirement to replace log everywhere in Groovy 5. The CGLIB case was verified in the application stack but has not been isolated to a particular compiler commit. Do not attribute it to the overload-dispatch change below without a separate reproducer.
BaseMessage cannot select an overload accepting a particular subclass. <T extends BaseMessage> still exposes the base type to static selection and did not fix the compared example. The compiler is not generally preferring the least-specific runtime type; Groovy 4's second, reflective dispatch had concealed the distinction.This comparison used both plain @CompileStatic classes and transactional services, without Hibernate proxies:
| Compared path | Groovy 4 stack | Groovy 5.1.3 stack |
|---|---|---|
| Ordinary static call with a base-typed parameter to public overloads | Object overload | Object overload |
| Static wrapper, then closure calling protected overloads | Runtime child overload | Statically selected Object overload |
| Static caller into a transactional overloaded method | Runtime child body after reflective redispatch | Selected Object body |
Same transactional call with <T extends BaseMessage> | Runtime child body after reflective redispatch | Selected Object body |
| Explicitly dynamic caller into public overloads | Runtime child overload | Runtime child overload |
If the application's contract is runtime overload selection, mark only that dispatch boundary dynamic:
import groovy.transform.CompileDynamic
@CompileDynamic
String dispatch(BaseMessage message) {
handler.process(message)
}Keep the typed overload implementations statically compiled and retain process(Object unsupported) as the unsupported-type guard. Do not replace the guard with an instanceof router before identifying the changed dispatch. Compare concrete instances with proxies separately: runtime dispatch does not itself unwrap a base-class Hibernate proxy. Verify every supported subtype and the unsupported-type path through public entrypoints.
Object where earlier versions accepted a more specific type. Prefer typed locals across try/catch and Map.get(null) over ambiguous null-key subscript expressions; do not disable static checking broadly.Class object to one of the represented domain types, inspect the generated code. Explicit Class<?> locals with if/else assignments fixed the checked application case. Treat this as a targeted compiler workaround, not a reason to replace every ternary.first in transactional code contaminated the generated setTargetDatastore method's inferred type, producing a cast of the datastore to an unrelated application class. Confirm the generated bytecode and use a descriptive local name as a narrowly scoped workaround; do not change datastore wiring to satisfy that cast. Recheck the framework/compiler fix before generalizing this snapshot defect.@CompileStatic: compiling the superclass and subclass together succeeds, but compiling the subclass against superclass bytecode reports unable to resolve class E at line -1. The standalone reproducer passes on Groovy 4.0.33 and fails on 5.0.8, 5.1.2, and 5.1.3, including JDK 21 and 25 for 5.1.3. Replace the affected ternary with if/else assignments explicitly cast to the concrete map type, and reference the issue beside that code. This localized workaround allows GroovyCompile.options.incremental = true. Verify with a clean --no-build-cache --rerun-tasks build, followed by a source edit and an incremental --no-build-cache --info build without --rerun-tasks; also compile the subclass alone against superclass bytecode to isolate the reported defect. Disabling incremental compilation is a fallback if affected code cannot be localized. Check the issue's fix version before removing the source workaround. Do not call it a Gradle defect solely because an incremental build exposes it.Review application configuration after booting with the Spring Boot properties migrator.
spring.data.mongodb.* to spring.mongodb.*.mongodb.* properties for this Spring Boot rename.spring.jackson.* was reorganized. Watch for migrator output such as spring.jackson.read.* moving to spring.jackson.json.read.* and spring.jackson.write.* moving to spring.jackson.json.write.*.spring.devtools.livereload.enabled: true under the development environment if needed.META-INF/build-info.properties by default.management.endpoint.health.probes.enabled: false only if required.grails.mail.overrideAddress is set, it now also replaces a from set in sendMail. To keep the application's sender while still redirecting every recipient, use grails.mail.overrideToAddress in its place.grails.controllers.upload.* property with its spring.servlet.multipart.* equivalent. Grails 8 rejects the old namespace at startup. Set explicit per-file and per-request limits to preserve existing uploads, and check the servlet container's overall request limit as well. An unspecified multipart location now uses the container's temporary directory.common ships common.properties or common-<purpose>.properties, with matching locale variants. Rename generic plugin bundles such as messages.properties; generateI18nDescriptor rejects colliding names. Application bundles still use the application message-source conventions.ResourceBundleMessageSource; application calls to old Grails-specific methods such as getMergedPluginProperties fail. For a known code catalog, enumerate its namespaced ResourceBundle keys and resolve the text through the public MessageSource API so application overrides remain effective. Test catalog/search and validation-error paths as well as individual message lookups.includeAll encounters compiled script/closure .class files beside Groovy changelogs, restrict discovery with endsWithFilter: '.groovy'. Verify existing source changelogs are still discovered; do not alter already-applied changesets to suppress resource warnings.Metadata.current.getProperty('info.app.name', String, null) and getProperty('info.app.version', String, null) rather than subscripting Metadata. The convenience getters getApplicationName() and getApplicationVersion() read those same keys; applicationName and applicationVersion are their Groovy bean-property spellings, not different configuration keys. The name getter supplies grailsApplication as its default, and the version getter supplies null.grails.config.locations, including development-only blocks and home-directory overrides. A ConfigSlurper stack trace naming a generated Script_... class can originate there; test-environment startup does not exercise those branches. Replace remaining Metadata.current['info.app.name'] and grailsApplication.metadata['info.app.version'] lookups with their typed accessors, and keep private local configuration out of commits.spring.http.client.connect-timeout and read-timeout to spring.http.clients.*, and spring.http.client.factory to spring.http.clients.imperative.factory in Boot 4.1. Preserve the chosen HTTP implementation and existing timeout values.management.endpoints.enabled-by-default with management.endpoints.access.default (none or unrestricted as appropriate), and per-endpoint enabled flags with access levels. Check web exposure independently, because accessibility and exposure are separate settings.server.maxHttpHeaderSize / server.max-http-header-size with server.max-http-request-header-size and an explicit data size, such as 128KB. Reconcile duplicate old/new declarations so the effective request-header limit is retained.Micronaut-enabled Grails 8 applications have stricter requirements than ordinary Grails 8 applications.
enforcedPlatform, not plain platform.grails-micronaut-bom for the default Micronaut setup.grails-hibernate5-micronaut-bom for Micronaut with Hibernate 5.grails-hibernate7-micronaut-bom for Micronaut with Hibernate 7.BootArchive.loaderImplementation = LoaderImplementation.CLASSIC; Spring Boot 4 removed the CLASSIC loader.grails { micronautAutoSetup = false } unless the build intentionally owns all Micronaut annotation processors and BOM validation.Example:
dependencies {
implementation enforcedPlatform("org.apache.grails:grails-micronaut-bom:$grailsVersion")
implementation 'org.apache.grails:grails-micronaut'
}Grails 8 follows Spring Boot 4 and Jackson 3.
com.fasterxml.jackson.annotation.*.com.fasterxml.jackson.databind.* to tools.jackson.databind.*.tools.jackson.databind.json.JsonMapper, not Jackson 2 ObjectMapper.JsonMapper.builder().build() for direct mapper construction.JacksonException types.spring.jackson.use-jackson2-defaults: true.For custom mapper configurations:
JsonMapper, retaining bean names/qualifiers and the intended primary bean. An ObjectMapper return type may not suppress Boot's format-specific default. Verify the real application context, not only test mapper replacements. Custom builders can use configureForJackson2() to preserve published defaults while running Jackson 3; Boot properties do not configure manually constructed mappers.ValueSerializer, ValueDeserializer, and SerializationContext; modifiers use ValueSerializerModifier / ValueDeserializerModifier with BeanDescription.Supplier. Configure mix-ins on the builder before building the immutable mapper, including MixInResolver.hasMixIns() and snapshot() for custom resolvers. Jackson 3 includes java.time support; retain only required date-format overrides. Migrate databind annotations such as JsonSerialize, while retaining shared Jackson annotations.+00:00 versus Z. Exercise form authentication, JSON, multipart and binary HTTP paths. In Spring 7, RestClient.Builder.configureMessageConverters starts with an empty builder: call registerDefaults() before editing its converter list, then use JacksonJsonHttpMessageConverter; otherwise even String form bodies can lose their converter.jackson-annotations, and check actual replacement releases. Libraries compiled against Jackson 2 cannot use Jackson 3 through exclusions or version substitution. Also search legacy wrapper classes such as Batch's Jackson2ExecutionContextStringSerializer: changing persisted execution-context JSON requires compatibility tests against existing rows, not just a new-serializer round trip.Spring Boot helper renames include:
| Jackson 2 helper | Jackson 3 helper |
|---|---|
Jackson2ObjectMapperBuilderCustomizer | JsonMapperBuilderCustomizer |
JsonObjectSerializer | ObjectValueSerializer |
JsonValueDeserializer | ObjectValueDeserializer |
@JsonComponent | @JacksonComponent |
@JsonMixin | @JacksonMixin |
JSON views now use groovy.json.JsonGenerator. If custom JSON view converters used Grails-specific generator classes, migrate them to groovy.json.JsonGenerator.Converter and rename the service loader file to src/main/resources/META-INF/services/groovy.json.JsonGenerator$Converter.
Enum serialization changed:
SimpleEnumMarshaller is the default for JSON and XML enum serialization.render(MyEnum.VALUE as JSON) now throws ConverterException; render an object, map, or explicit string instead.For an application using Swagger/JAX-RS scanning solely to document Grails controllers, adopt implementation 'org.apache.grails:grails-openapi' and retain its standard @Operation, @ApiResponse, @Parameter, and @Schema annotations. See the target version's OpenAPI guide, checking the branch as described above.
generateOpenApi Gradle task through its auto-provisioned CLI companion. Use that task; do not recreate it or write a custom document generator. Remove the old Swagger Gradle plugin, its buildscript dependency, JAX-RS scanner and documentation-only JAX-RS annotations.grails.openapi.base-document to retain the existing title, servers, security schemes, tag descriptions and viewer extensions. Use annotated-only and paths-to-match to retain the intended public API selection.@Path. Reconcile base-document server prefixes so /api/v1 is not duplicated, and rename @Parameter(in = PATH) declarations to match actual mapping variables. Verify non-GET operations against the mappings.metaClass and validateable errors properties are already excluded; remove a custom model converter whose only purpose was hiding those properties.grailsCli, not the deployed runtime.build/openapi, then include the output when packaging or running the application. Do not put generated documentation into sourceSets.main.output or make processResources depend on generation: the command itself needs classes, creating a dependency cycle./v3/api-docs or Swagger UI is wanted. Test the generated contract, schema references, and the served document, not just task success.Do not assume a Grails 8 upgrade requires Hibernate 7.
org.springframework.orm.hibernate5 with org.grails.orm.hibernate.support.hibernate5 for Hibernate 5 or org.grails.orm.hibernate.support.hibernate7 for Hibernate 7.Hibernate 7 opt-in example:
dependencies {
implementation enforcedPlatform("org.apache.grails:grails-hibernate7-bom:$grailsVersion")
implementation 'org.apache.grails:grails-hibernate7'
}If moving from Hibernate 5 to Hibernate 7, audit direct Hibernate API use:
| Hibernate 5 call | Hibernate 7 replacement |
|---|---|
session.save(entity) | session.persist(entity) |
session.update(entity) | session.merge(entity) |
session.saveOrUpdate(entity) | session.persist(entity) or session.merge(entity) based on entity state |
session.delete(entity) | session.remove(entity) |
session.load(Class, id) | session.getReference(Class, id) |
session.get(Class, id) | session.find(Class, id) |
Also check for:
@Where, @WhereJoinTable, @Proxy, @LazyCollection, @Persister, @SelectBeforeUpdate, and @Loader.CascadeType.SAVE_UPDATE; use CascadeType.ALL, CascadeType.PERSIST, or CascadeType.MERGE as appropriate.refresh() or lock() calls, which now throw IllegalArgumentException.java.time date/time types instead of java.sql date/time types.StatelessSession participating in second-level cache by default. Set CacheMode.IGNORE for intentional cache bypass.jakarta.persistence usage.GORM behavior changes:
nullable: false on required domain properties, or set grails.gorm.default.nullable: false to restore the previous application-wide default.save(), delete(), get(), load(), and merge() are not affected by Hibernate Session API removals.namedQueries definitions and createNamedQuery calls to supported where / DetachedCriteria queries. Grails 8 removed deprecated query infrastructure; an old generated method name is not proof the underlying API remains supported. Retain every predicate and verify excluded, unrelated, and absent rows.sort and order arguments of list(), dynamic finders, where queries, criteria queries and listOrderBy* are validated on both Hibernate versions: sort must be a dotted property path that resolves through the mapping (a dotted alias such as c.name is still passed through), and order must be asc or desc. Anything else throws IllegalArgumentException (Invalid sort property / Invalid sort direction) instead of being ignored or failing inside Hibernate. On Hibernate 7, list() also rejects a fetch key that is not a persistent property.String before executeQuery or another checked GORM query method can fail compilation with GormUnsafeQueryString. Bind data values as parameters. When interpolation is solely for reviewed, fixed query fragments and values already use named parameters, document that distinction and suppress the check on that specific method rather than converting query syntax into bound values.withCriteria(uniqueResult: true) returned a PagedResultList; Grails 7 dispatched it to get. Use createCriteria().get { ... } for entity/scalar results, preserving cache and valid fetch joins in the DSL. Test both absent and present results, especially row-count projections where a nonempty list containing zero is truthy. Treat this as a version-specific regression, not a universal documented removal.REQUIRED joins an active transaction, while REQUIRES_NEW suspends it and uses a separate session, then restores the outer session. This also affects SimpleMap-backed domain/service unit tests. A thread-bound test session does not mean every service invocation shares one transaction or first-level cache; check the service annotations and explicit withNewTransaction calls before attributing changed fixture visibility to a regression.ident() and reload/proxy behavior for composite keys and renamed single-property identifiers. In the checked pre-release build, ident() read only the ordinary id property, returning null for a persisted composite-key domain and for a key mapped with id name: 'code'; Grails 7 used the mapped property or constructed a serializable identifier from the composite key fields. Consequently load(entity.ident()) returned null before any proxy was created. Regression tests reproduced both cases on Hibernate 5 and Hibernate 7. Treat this as a framework regression to isolate and fix, rather than excluding composite-key domains from coverage or interpreting the null as a proxy-initialization change.find still returns the original element even if its predicate unwrapped it; use findResult when the intended result is the unwrapped matching object. Equivalent warmed-call-site failures were reproduced on the old stack too, so distinguish a pre-existing proxy assumption from a new compiler regression.validate(), validation on save(), and dirty-instance flush separately. The verified Grails 7 behavior starts validation with fresh errors while retaining data-binding failures; manually added global reject(...) errors and ordinary rejectValue(...) errors are reset. Constraints and beforeValidate can then add current errors. deepValidate: false changes association validation, not that reset contract. Consult the target version's validate reference, checking its version as above.include: []: the latter binds nothing. Passing an empty list to a lower-level binding API is not a spelling of the default behavior. Keep deliberate include/exclude restrictions, bindable: false, and deny-by-default configuration intact; consult the target version's bindData reference.lineItem.product.id. Put a lookup code on its associated entity's ID field, not the containing object's unrelated numeric ID. Test the real parameter structure, including any values inserted by the controller, and retain a negative test for protected properties.bindable: false. Reproduce through bindData / the public binding API on both versions before adding application-wide allowlists to compensate. Do not infer the same defaults for secure mode.Grails 8 supplies MIME type defaults from the framework.
grails.mime.types block.grails.mime.types block still replaces the defaults.grails.mime.mergeDefaults: true when adding custom MIME types while keeping built-in defaults.The HTTP Accept header is honored for all clients by default, including browsers.
text/html highest.fetch() or XMLHttpRequest calls requesting JSON now receive JSON without relying on X-Requested-With.respond actions without an HTML view may now error for browser requests because browsers negotiate HTML. Add a GSP view, use render, or scope formats with responseFormats or respond(..., formats: ...).grails.mime.disable.accept.header.userAgents explicitly.The HTML codec uses XML-safe escaping when grails.views.gsp.htmlcodec is not set, as applications generated with htmlcodec: xml already did.
htmlcodec: xml are unaffected; the setting can stay or be removed.é instead of é), and @, the backslash and the backtick are escaped. Update tests that compare escaped markup exactly.grails.views.gsp.htmlcodec: html4 only where pages are served in a character set that relies on the named entities, such as ISO-8859-1.An interceptor's model returns the map that an action passed to render(template: ..., model: ...) instead of null.
after() runs. Changing the map has no effect on the response, and changing an immutable map throws UnsupportedOperationException.model != null, or use model?., before changing the model. That code now also runs for template renders.bean or collection are not part of model.request instanceof MultipartHttpServletRequest / StandardMultipartHttpServletRequest checks and casts; changing only the concrete class to the interface still relies on the removed substitution.request.getFile(name), getFiles(name), getFileNames(), getFileMap(), getMultiFileMap(), and getMultipartContentType(name). File binding through params remains supported. Grails locates the resolved multipart request behind the wrappers or through the dispatcher-published attribute. When separate validation paths need a format check, inspect the multipart content type rather than the request's runtime class; upload accessors throw IllegalStateException on a request without resolved multipart data.ContentCachingRequestWrapper / JSON-body branch. An upload can now retain that outer wrapper, and an empty cached JSON/body buffer does not establish that its multipart file map is empty. Validate upload presence and part contents through the upload API, preserving the application's missing-part, empty-file, and non-multipart error responses.spring.servlet.multipart.*.Grails 8 uses asset-pipeline 5.2, where a % or * component of an asset path, in a require directive, an <asset:...> tag, or a Sass import, stands for exactly one directory wherever the asset is found.
% stand for several directories when the asset came from a jar, as every webjar does, or from the manifest of a packaged application. A path such as webjars/%/dist/jquery.js resolved in Grails 7 and resolves to nothing in Grails 8.% and * in asset paths. Write one % per directory, as create-app generates (webjars/jquery/%/dist/jquery.js), or %% (or **) for zero or more directories (webjars/%%/dist/jquery.js).*= require block, use % and %%, never * or **, because */ ends the comment.% per directory. Hidden directories never match, and a wildcard inside a file name, such as jquery-%.js, does not resolve.assetCompile logs Unable to Locate Asset: <path> and leaves the file out, and an <asset:...> tag renders the path unchanged, so the browser cannot load it. Check the assetCompile output for that warning after upgrading.includes and excludes patterns of the assets block in build.gradle are not affected.javetBaseUrl repository proxies the current platform/version, rather than only hosting older private releases.@import to explicit @use / @forward dependencies. Configure a shared theme before forwarding Bootstrap or other configurable libraries; module isolation changes variable visibility and cross-module @extend behavior. Compare generated theme values and representative selectors, not just compilation, and avoid introducing a second unthemed Bootstrap entrypoint. Dependency-internal deprecation warnings need an upstream library migration rather than blindly rewriting vendored sources.$.isArray with Array.isArray, $.isFunction with a function-type check, $.parseJSON with JSON.parse, and $.type with checks appropriate to the supported input types. Audit actual callers for boxed values or cross-window objects before assuming every replacement is equivalent.$.trim's handling of absent values: use String(value ?? '').trim() for nullable or numeric input, and native .trim() for known strings. value || '' incorrectly drops numeric zero. A replacement for $.isNumeric must deliberately handle finite numbers/numeric strings while rejecting null, booleans, blanks, and infinities; Number.isFinite(Number(value)) alone accepts several of those invalid inputs.$.camelCase. An exception during their initialization can prevent the whole form/table setup. Verify actual browser interaction, not only JavaScript syntax, server responses, or the presence of generated HTML.Grails 8 recommends method-based TagLib handlers while keeping closure-based tags supported.
def tag(Map attrs), def tag(Closure body), and def tag(Map attrs, Closure body) are discovered automatically.@grails.gsp.Tag.@grails.gsp.NotATag when they must not be exposed as tags.Map parameter named attrs receives the full attributes map.Closure parameter named body receives the tag body.grails { preserveParameterNames = true } is the default for Groovy compilation.Map attrs and Closure body parameters, return-object declarations, and encoding behavior. Audit test code that assigns closures to tag properties; use method-aware mocks with cleanup instead. Helper classes stored under grails-app/taglib are not necessarily tag libraries, so do not mechanically convert every closure in that directory.sitemeshTagLib.applyLayout = { ... } when the method-based tag API no longer exposes that closure property. Exercise the real layout/rendering path in integration tests; replacing the tag namespace can conceal the behavior the upgrade needs to verify.tf.with(attrs, body) rather than directly calling with(attrs, body). Direct method dispatch can write into the outer output and return an OutputProxyWriter, placing inputs outside a form or rendering the writer's identity. Use real runtime GSP rendering to verify markup nesting and returned text; taglib unit-test metaclasses can hide the defect.grails-layout with grails-sitemesh3, and use grails.sitemesh.default.layout. Replace custom inheritance from RenderGrailsLayoutTagLib and direct GSPGrailsLayoutPage access with public g:applyLayout, g:pageProperty, and g:ifPageProperty tag delegation. Verify nested layouts, captured page properties, titles, and email layouts.Test changes:
purgeTagLibMetaClass properties or getters from TagLib specs.GrailsWebUnitTest and calls mockTagLib directly, move the call from setupSpec() to setup().mockTagLib(MyTagLib) registration over replacing a controller's namespace property or constructing a custom namespace dispatcher. Compiled tag calls resolve through the registered taglib bean and can bypass controller metaclass stubs. Despite its name, mockTagLib(Class) registers a real, autowired bean through defineBeans when absent, registers its tags, and returns it; it does not create a Spock mock or spy.mockTagLib(Class), which reuses the bean. A bean definition with instanceSupplier = { -> tagSpy } can supply the instance; retain autowiring when the taglib needs injected collaborators. Wrapping the returned real bean with Spy(...) alone does not replace the instance used by tag dispatch. The checked Grails version has no instance-taking mockTagLib overload; do not present an application helper overload as a framework API.tagSpy.out; returning a string alone does not reproduce captured writer output. Stub object-returning tags according to their declared return contract. Use Spock's @ConfineMetaClassChanges for every class whose metaclass a test changes, including global Groovy mocks; automatic Grails tag metadata cleanup does not replace cleanup of unrelated test mutations.href can correctly contain & where the destination URL contains &. Update the expected markup while preserving the destination and query values, rather than disabling attribute escaping to match an old string fixture.>> { } rather than >> null when suppressing real behavior on a spy. The latter produced a null-to-void coercion failure under the checked Groovy 5 / Spock combination.refresh() for updated entities or reload after clearing the session / in a fresh session for persisted-state/deletion assertions. Compare persistent identifiers when the domain uses instance equality. Apply this at verified transaction boundaries, rather than adding blanket flush/refresh calls. SimpleMap flushing makes its writes available to another session; it is not proof of database transaction isolation. In database-backed integration tests, flush() is not a commit, so independently transactional work needs fixtures committed outside the suspended transaction.findOrBuild treats its map as finder/domain values: use findOrBuild(...).save(flush: true), rather than adding a flush property to that map. Its argument handling differs from build(flush: true, ...).flash.now map when resetting it in tests rather than assigning a replacement map to its read-only property. Retire test shims when the supported framework behavior now supplies that state.grails.gorm.default.nullable as well as runtime validation. A pre-release testing-support defect ignored the configured required-by-default setting, so build-test-data skipped required associations and later reported missing constraints on their entities. Check a minimal domain validation test before adding more mocks; use a framework build with the constraint-evaluator configuration fix.@SpringBootTest no longer auto-configures MockMvc, WebClient, or TestRestTemplate. Add @AutoConfigureMockMvc, @AutoConfigureWebClient, or @AutoConfigureTestRestTemplate as needed.@MockBean and @SpyBean with @MockitoBean and @MockitoSpyBean from org.springframework.test.context.bean.override.mockito.MockitoTestExecutionListener registration.TestRestTemplate imports to org.springframework.boot.resttestclient.org.junit.platform:junit-platform-launcher to custom test runtime configurations when the testing plugins do not already supply it; Gradle 9 no longer supplies an implicit launcher.grails.util.GrailsWebMockUtil now live in test fixtures. Tests using it need testFixtures('org.apache.grails.web:grails-web-common'). If production code used it to run a controller on a background thread, prefer shared service calls, or real authenticated HTTP requests when controller binding, filters, interceptors and rendering are required. Merely constructing a GrailsWebRequest around Spring mocks still ships test utilities and bypasses the real request lifecycle. Remove the production spring-test dependency and verify the resolved runtime classpaths, keeping fixtures in test or CLI-only configurations.OffsetDateTime values. Update tests that assumed those always bind to null; keep separate assertions for custom screen formats and zone-bearing input.org.testcontainers:postgresql to org.testcontainers:testcontainers-postgresql and org.testcontainers:spock to org.testcontainers:testcontainers-spock.Test.ignoreFailures = true; a successful Gradle exit does not establish passing tests.Interceptor.throwable is a Throwable, not necessarily an Exception. A pre-release error-handling defect cast the servlet error attribute to Exception, causing a second GroovyCastException for initialization or assertion errors before afterView could run. Use a framework build with the Throwable-handling fix and inspect the earlier exception to diagnose the original application failure; fixing error reporting does not fix its cause.response.contentAsString / contentAsByteArray instead of calling getOutputStream() merely to inspect a response that used the writer.response.reset(), making a subsequent CSV/binary download fail even when its query and generated bytes were correct. The framework correction leaves output selection lazy. Verify writer-to-reset-to-stream and stream-to-reset-to-writer with the real bound Grails web request. Remove temporary alternate-response or empty-export workarounds once the fixed framework is used.Check plugin compatibility before treating a Grails 8 runtime failure as an application bug.
grails-layout applications can retain that layout plugin.RoleHierarchyImpl is now final and constructed with fromHierarchy; implement RoleHierarchy and delegate if the application reloads hierarchy data. Legacy org.springframework.security.access.event classes are removed. Modern AuthorizationEvent exposes authentication through a supplier; verify that the configured authorization chain actually publishes the events the application needs.hierarchy assignments from the Grails Spring Security plugin during startup. Preserve that setter when changing from inheritance to delegation, and test that startup assignment does not suppress lazy database initialization or later resets.DaoAuthenticationProvider takes UserDetailsService in its constructor; replace the removed setter in application Spring DSL and test fixtures, then set the password encoder separately.portResolver assignments from custom LoginUrlAuthenticationEntryPoint / AjaxAwareAuthenticationEntryPoint bean definitions. Their Spring Security 7 API no longer exposes that setter; retain supported settings such as portMapper and redirectStrategy. Check the specific receiving class rather than deleting every portResolver reference: other security components still use it. Exercise development/deployed-only bean definitions, since a test profile may skip the wiring entirely.DefaultSavedRequest no longer takes a PortResolver; construct it with the request and verify the resulting saved-request/redirect flow.GebGrailsPlugin also has the logical name geb, even in another package, and can conflict with Grails' Geb plugin so test bootstrap never runs. Give the application plugin a distinct class/logical name, clean its generated descriptor/classes, and run a real login/bootstrap smoke test.JobExecution.getJobInstanceId(); update mocks as well as callers. JDBC execution lookup can throw EmptyResultDataAccessException for an absent row where older code expected null; preserve the application's lookup contract and test missing and persisted executions..chunk(size, transactionManager) retains the deprecated tasklet engine; .chunk(size).transactionManager(transactionManager) selects ChunkOrientedStep. In the new engine, the chunk task executor covers processing rather than the whole read/process/write transaction. If a synchronous executor establishes accounting/security context, wrap the entire transaction callback (including transaction creation and commit/rollback) with that context; do not assume a processor executor preserves the old boundary or apply this blindly to asynchronous partition executors.final only where proxy dispatch is needed and test multiple actual step scopes.spring-boot-starter-batch-jdbc or an explicit JdbcDefaultBatchConfiguration when retaining a database repository and its sequences.grails-spring-websocket 3.0.x targets Grails 8 / Boot 4 / Jackson 3; 2.7.x targets Grails 7. Verify the actual broker's JSON converter and routed payloads after upgrading, not just dependency resolution.grails.gorm.annotation.AutoTimestamp can fail even if the application does not use that annotation. Upgrade the plugin or fix its optional annotation lookup; adding application mocks does not fix the class-linkage error. Plugins doing configuration work in doWithSpring must use the supplied grailsApplication and avoid prematurely instantiating Spring Security services during bean registration.SynchronizerTokensHolder: withForm now validates and consumes tokens through isValidAndResetToken(url, token). Overriding only resetToken no longer preserves an application's reusable AJAX/session token. The characteristic failure is that the first AJAX POST succeeds, then later requests fail CSRF validation because the token URL has disappeared from the holder.super.isValidAndResetToken so duplicate submissions are still rejected atomically; do not bypass CSRF validation for AJAX requests.isValid and isEmpty consistently when those methods are used. Include recovery of a persisted holder whose map entry was already consumed by the old implementation.Run verification through public application behavior, not only compilation.
./gradlew clean check for the application, or the closest module-specific equivalent when the full suite is too expensive.fetch() flows, file uploads, custom withFormat or respond actions, TagLib-rendered views, and persistence paths.// Required domain property after Grails 8 nullable-default change
class Book {
String title
static constraints = {
title nullable: false
}
}# Restore legacy required-by-default domain validation temporarily
grails:
gorm:
default:
nullable: false# Add a custom MIME type while keeping Grails 8 defaults
grails:
mime:
mergeDefaults: true
types:
custom: application/vnd.example+json// Spring Boot 4 test bean override
import org.springframework.test.context.bean.override.mockito.MockitoBean
@SpringBootTest
class BookServiceSpec extends Specification {
@MockitoBean BookRepository bookRepository
}grails.mime.types block if the intent is to extend the new defaults. Use grails.mime.mergeDefaults.purgeTagLibMetaClass in tests. It is removed.javax.* imports. Grails 8 continues the Jakarta baseline.© apache, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in grails-skills/upgrade-guide-8/skills/grails-8-upgrade of apache/grails-core.
Open the folder on GitHubat commit 4deae57
Grails 8 Upgrade Guide next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Grails 8 Upgrade Guide this skillapache/grails-core | 2.9k | — | ~20k | Automated safety check: Pass | Apache-2.0 | |
| WxJava Upgrade Guidebinarywang/WxJava | 33k | — | ~129 | Automated safety check: Pass | Apache-2.0 | |
| Dependabot PR Reviewkernitus/BukkitOldCombatMechanics | 225 | — | ~882 | Automated safety check: Pass | MPL-2.0 | |
| Javaericrisco/rsc-harness | 167 | — | ~4.6k | Automated safety check: Pass | MIT | |
| Security Vulnerabilities Patcheraxelixlabs/axelix | 148 | — | ~4.2k | Automated safety check: Pass | LGPL-3.0 | |
| Neo4j Driver Java Skillneo4j-contrib/neo4j-skills | 114 | — | ~4.2k | Automated safety check: Notes | MIT |
binarywang/WxJava
Plans a WxJava version upgrade, checking the BOM, module dependencies, JDK version and configuration, with phased steps and clear rollback conditions.
kernitus/BukkitOldCombatMechanics
A skill your agent uses for Dependabot PRs, dependency bumps, Gradle or Maven dependency updates, GitHub Actions updates, dependency changelog/licence/release-note review, JVM/classfile checks, and…
ericrisco/rsc-harness
A skill your agent uses when writing, reviewing or refactoring modern Java (21+, 25 LTS) — records and sealed interfaces as algebraic data types, exhaustive pattern-matching switch over instanceof…
axelixlabs/axelix
Create batched Dependabot-style pull requests for GitHub security findings in axelixlabs/axelix, grouped by dependency surface such as master/front-end, master/build.gradle.kts, or starter Gradle…
neo4j-contrib/neo4j-skills
Neo4j Java Driver v6 — driver lifecycle, Maven/Gradle setup, executableQuery, executeRead/Write managed transactions, explicit transactions, async/reactive patterns, error handling, data type…
sivaprasadreddy/sivalabs-agent-skills
A skill your agent uses when the user wants to create/generate/scaffold a new Spring Boot project (Maven or Gradle, REST API / Web App / Spring Boot + Angular full stack).
apache/grails-core
Guides building Grails web applications and REST APIs with GORM, controllers, services, views, plugins and Spock and Geb testing.
apache/grails-core
Guidance for Groovy 5 work in Grails projects: syntax, closures, traits, DSLs, metaprogramming, Spock tests, static compilation and Java 21 integration.
apache/grails-core
Guides changes to the grails-data-hibernate7 module, covering domain binding, Hibernate 5 to 7 migration work, generators and integration specs.
apache/grails-core
Guide for writing modern Java 21 in a Grails and Groovy codebase: records, sealed classes, pattern matching, text blocks and how Java works alongside Groovy.
apache/grails-core
Guides running, reviewing and fixing test failures across grails-core modules with Gradle, including targeted runs and the aggregate HTML and Markdown reports.
apache/grails-core
Guide to running, reading and fixing code style and analysis violations in grails-core with CodeNarc, Checkstyle, PMD, SpotBugs, Spotless and JaCoCo through Gradle.
Works with
Categories
Guides moving a Grails application from 7.x to Grails 8 and turns the official upgrade guide into a checklist of breaking changes. The skill treats the published Grails documentation as the source of truth and turns the Grails 8 upgrade guide into a practical migration checklist focused on public application behavior, not framework internals.1, Spring Framework 7, Jackson 3, the Gradle platform, Micronaut, Hibernate, TagLibs, testing, validation and content negotiation.
Grails 8 Upgrade Guide fits situations like: upgrading a Grails 7.x application to Grails 8; reviewing a Grails 8 upgrade branch or pull request; fixing build, test or runtime failures after the move to Grails 8; deciding between staying on Hibernate 5 and opting in to Hibernate 7.
Run `npx skills add apache/grails-core --skill grails-8-upgrade -a claude-code`. Or copy the skill folder (grails-skills/upgrade-guide-8/skills/grails-8-upgrade in apache/grails-core) into .claude/skills/grails-8-upgrade in your project. Claude Code loads it when a task matches its description.
Run `npx skills add apache/grails-core --skill grails-8-upgrade -a codex`. Or copy the skill folder (grails-skills/upgrade-guide-8/skills/grails-8-upgrade in apache/grails-core) into .agents/skills/grails-8-upgrade in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add apache/grails-core --skill grails-8-upgrade -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/grails-8-upgrade, .gemini/skills/grails-8-upgrade, .github/skills/grails-8-upgrade and .opencode/skills/grails-8-upgrade in your project.
SKILL.md names no scripts, command-line tools or credentials: Grails 8 Upgrade Guide is instructions for the agent only. Our summary lists: A Grails 7.x application with a Gradle build; Access to the published Grails documentation.
SKILL.md names 3 domains. In commands or code: grails.apache.org and github.com; the agent is likely to contact these when it follows the instructions. As links in the text: issues.apache.org. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Grails 8 Upgrade Guide is published under the Apache-2.0 licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.
About 20k tokens (SKILL.md is roughly 80k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Grails 8 Upgrade Guide: WxJava Upgrade Guide (binarywang/WxJava, 33k stars), Dependabot PR Review (kernitus/BukkitOldCombatMechanics, 225 stars), Java (ericrisco/rsc-harness, 167 stars) and Security Vulnerabilities Patcher (axelixlabs/axelix, 148 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
apache (a GitHub organization) maintains it in apache/grails-core, which has 2,934 GitHub stars. The repository holds 9 skills in this directory. The repository was last updated on October 8, 2026.
Source: apache/grails-core on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.