jdaugherty commented on code in PR #16237:
URL: https://github.com/apache/grails-core/pull/16237#discussion_r4257468488
##########
grails-doc/src/en/guide/upgrading/upgrading90x.adoc:
##########
@@ -46,3 +46,316 @@ The Groovy actor event bus delivers each event to its
subscribers in publication
Applications that use GPars directly, for example `GParsPool.withPool {
list.eachParallel { } }`, should move to the Groovy equivalents: the parallel
collection methods are available on every `Collection` without a pool, and
`groovy.concurrent.ParallelScope.withPool(n) { scope -> list.eachParallel { }
}` runs them on a pool of a chosen size. Once the deprecated modules are
removed, Grails will no longer bring GPars onto the classpath, so an
application that still needs it must declare `org.codehaus.gpars:gpars` itself.
NOTE: Groovy 6 marks the concurrency toolkit as incubating. The Grails modules
that build on it expose only the Grails `Promise` and `EventBus` contracts, so
changes to the Groovy API in later Groovy releases are absorbed by Grails
rather than by application code.
+
+==== 2. XML Web Support Moves to a Separate Module
+
+XML conversion, request-body binding, codecs, and REST renderers now live in
the optional
+`grails-xml` module. The web starter still includes it in Grails 9, preserving
existing XML endpoints.
+JSON-only applications can exclude `org.apache.grails:grails-xml` from their
starter dependency now.
+In Grails 10 the starter will stop including it and `@Resource` will default
to JSON only.
+Legacy XML conversion logs one deprecation warning on use and is scheduled for
removal in Grails 11.
+MIME type negotiation still recognizes `application/xml`, `text/xml`, Atom,
and other XML media
+types; recognizing a media type does not install an XML serializer.
+
+New REST controllers, RESTful controllers, resources, and scaffolded
controllers generated by the
+`rest-api` profile advertise JSON only. After adding `grails-xml`, add `xml`
to the artefact's
+`responseFormats` or `formats` declaration when that endpoint should negotiate
XML.
+
+Applications that expose XML endpoints or use `grails.converters.XML`, XML
object marshallers, XML data
+binding, or the XML and Atom renderers need this module. It is supplied
transitively by the Grails 9
+web starter; applications without that starter, and applications upgrading to
Grails 10, add it explicitly:
+
+[source,groovy]
+.build.gradle
+----
+dependencies {
+ implementation 'org.apache.grails:grails-xml'
+}
+----
+
+The module preserves the established Grails XML APIs and compatibility
marshalling for domain-shaped
+payloads, collections, maps, validation errors, named converter
configurations, and include/exclude
+projections.
+
+Set `grails.web.rendering.xml.spring: true` to write ordinary bean responses
with Spring Framework's `JacksonXmlHttpMessageConverter`,
+backed by the Spring Boot-managed Jackson XML mapper. As in a plain Spring
Boot application, that
+converter is registered only when Jackson's XML dataformat is on the
classpath, so applications that
+want it add the dependency themselves:
+
+[source,groovy]
+.build.gradle
+----
+dependencies {
+ runtimeOnly 'tools.jackson.dataformat:jackson-dataformat-xml'
+}
+----
+
+Grails 9 defaults this setting to `false`: adding the Jackson XML dependency
alone does not bypass
+registered XML marshallers. Enabling it explicitly selects Spring for ordinary
beans, even when a
+legacy marshaller is registered. Domain objects, collections, maps, validation
errors and projections
+continue to use the Grails XML converter; migrate these to DTOs or explicit
application renderers
+before legacy XML conversion is removed in Grails 11.
+
+Without the Jackson XML dependency, `grails-xml` still renders XML through the
Grails converter. Applications may also register
+their own Spring XML message converter or OXM/JAXB infrastructure through
public Spring configuration
+APIs.
+
+This change does not affect Grails plugin descriptors. Plugin metadata
continues to be generated and
+published in its existing descriptor format; `grails-xml` concerns application
HTTP payloads only.
+
+==== 3. Opt Into Spring JSON Conversion
+
+Grails 9 preserves the legacy `grails.converters.JSON` converter for
`respond()` by default and logs one
+deprecation warning when that path renders. Opt into Spring MVC's message
converters, backed by Boot's
+configured Jackson `JsonMapper`, with `grails.web.rendering.json.spring:
true`. Grails 10 will enable it
+by default; the legacy `respond()` path is scheduled for removal in Grails 11.
Jackson can render date
+formats, bean properties, circular references, and validation errors
differently, so review the JSON
+returned by `respond()` endpoints when you upgrade. Explicit `render value as
JSON` calls still use the
+legacy converter.
+
+The setting alone determines the default response path: registering a
marshaller does not implicitly
+change it. `false` retains the legacy converter (with the warning); `true`
uses Spring's converters
+even while legacy marshallers are registered. Migrate those representations to
Jackson serializers
+before enabling it. Legacy pretty-print settings do not configure Jackson; use
+`spring.jackson.serialization.indent-output` on the Spring path. Renderer
beans an application
+declares, such as a `JsonRenderer` or an `XmlRenderer` for one of its types,
follow the same settings
+and reach the same named configurations and message converters.
+
+[source,yaml]
+.application.yml
+----
+grails:
+ web:
+ rendering:
+ json:
+ spring: true
+----
+
+Grails selects the first configured MVC `HttpMessageConverter` that can write
the
+response type and negotiated JSON media type and advertises a JSON media type
for that class
+(`application/json`, `text/json`, or a `+json` subtype). Generic `*/*` string
and byte-array converters
+are skipped, so strings remain JSON strings and byte arrays use Jackson's
base64 JSON representation.
+The same media-family rule applies to XML conversion; a generic text converter
cannot strip the XML
+string element.
+
+The standard Jackson converter uses an isolated mapper derived from Boot's
configured `JsonMapper`
+for Grails domain compatibility. Persistent properties, identifiers,
association identifiers, and
+per-response domain projections follow Grails metadata. On this domain path,
Jackson property
+annotations (`@JsonIgnore`, `@JsonProperty`, `@JsonInclude`, `@JsonView`,
`@JsonFormat`), property
+mixins, naming strategies, and transient getters do not define the
representation. Register an
+explicit serializer or return a DTO when that property model is required.
Ordinary non-domain beans
+retain Jackson's property rules. Application-provided converter subclasses and
per-type mapper
+registrations keep their configured behavior and precedence.
+
+Association identifiers use their mapped identity property name (for example
`uuid`), rather than
+the legacy marshaller's fixed `id` key. A value that leads back to a domain
object still being
+written, such as an embedded value that points at its owner, is written as the
legacy converter
+writes it: `{"_ref": "..", "class": "..."}` by default, or as
+`grails.converters.json.circular.reference.behaviour` selects. With `IGNORE`,
the property is written
+as `null` where the legacy converter left its name without a value. A cycle
made only of non-domain beans
+follows Jackson's rules, as it does in a plain Spring MVC controller, and
fails the response.
+
+Boot's shared mapper is not given the Grails domain serializer, so plain
Spring `@RestController`
+responses retain Jackson's normal domain property model. Both mappers
serialize Groovy `GString`
+values as strings. Only the Grails mapper serializes Spring `Errors` as an
`errors` array containing
+object, field, message, and codes, without rejected values; Grails `respond
errors` adapts that array into an
+RFC 9457 problem response.
+
+Grails retains the legacy converter path when a renderer sets a legacy named
converter
+configuration, when a per-response `includes`/`excludes` projection is
requested and the selected
+converter is not Spring's standard Jackson converter, or when no configured
MVC converter can write
+the selected type and media type. Explicit application and plugin `Renderer`
beans continue to take
+precedence over the default renderer.
+
+Jackson conversion uses UTF-8 for intermediate bytes; the response writer
applies
+`grails.converters.encoding`, so non-UTF response encodings do not corrupt the
intermediate JSON.
+
+Controller unit tests use Boot's Jackson configuration and the same setting.
Supply a mapper or
+`JsonMapperBuilderCustomizer` from a nested Spring `@Configuration` class, so
Boot can see it before
+evaluating its conditional beans. A mapper registered by the deprecated
`doWithSpring` callback is too
+late to make Boot back off. Named JSON
+configurations work through `render` and `respond` in `ControllerUnitTest`.
The optional `grails-xml`
+beans are contributed by web test traits when present, without loading the XML
plugin's dependency
+graph. Web traits automatically include `restResponder`, even when a test
overrides
+`getIncludePlugins()`, and provide JSON conversion beans and Boot's mapper
configuration.
+They do not automatically load the controllers, URL mappings or i18n plugins;
link generation retains
+the curated unit-test setup. Plain `GrailsUnitTest` and `ServiceUnitTest` do
not load XML or the web
+plugin graph simply because that module is on the classpath.
+Web unit tests build Spring MVC's message converters from the test classpath
and configuration; see
+<<unitTesting,Unit Testing>>.
+
+==== 4. JSON Converter Customization Moves to Jackson
+
+The `grails.converters.JSON.registerObjectMarshaller(...)` API remains
supported for
+`render ... as JSON` and converter-based rendering. Registering a marshaller
does not change the
+`respond()` rendering setting. Marshaller registrations apply to its legacy
converter path, not to
+Spring's message converters. To customize the Spring path, configure Spring
Boot's `JsonMapper` so
+the same serializer is used by opted-in `respond`, Spring MVC message
conversion, and other
+Jackson-based integrations:
+
+[source,groovy]
+----
+import
org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer
+import tools.jackson.databind.module.SimpleModule
+
+@Bean
+JsonMapperBuilderCustomizer bookJsonCustomizer() {
+ { builder ->
+ builder.addModule(new SimpleModule()
+ .addSerializer(Book, new BookValueSerializer()))
+ } as JsonMapperBuilderCustomizer
+}
+----
+
+`BookValueSerializer` is an application-provided Jackson
`ValueSerializer<Book>`. Use a module when several
+serializers belong together, or register an individual serializer directly
through the builder customizer.
+
+For different JSON shapes selected by name, replace
`JSON.createNamedConfig(...)` with the injectable
+`NamedJsonConfigurationRegistry`. Each registration derives an isolated mapper
from Spring Boot's configured
+`JsonMapper`, so it retains application-wide Jackson settings without changing
the shared mapper:
+
+[source,groovy]
+----
+import grails.converters.json.NamedJsonConfigurationRegistry
+
+class BootStrap {
+ NamedJsonConfigurationRegistry namedJsonConfigurationRegistry
+
+ def init = { servletContext ->
+ namedJsonConfigurationRegistry.register('deep') {
+ it.serializer(Type, new TypeValueSerializer())
+ it.serializer(Category, new CategoryValueSerializer())
+ }
+ }
+}
+----
+
+Select the configuration per response with `respond`, or with the `json`
render argument:
+
+[source,groovy]
+----
+respond book, jsonConfiguration: 'deep'
+render json: book, jsonConfiguration: 'deep'
+// Without a name, use the default Jackson configuration:
+render json: [ok: true]
+----
+
+Both accept `includes` and `excludes`. A projection applies to the properties
of the type written,
+or of each element's type when a collection or array is written, unlike the
legacy converter, which
+projected only a single root object. It applies to domain objects and other
beans alike; objects of
+other types inside them are written in full. Properties are named by their
bean property names, and a JSON name given by a Jackson
+annotation or naming strategy matches as well. A value with no properties to
project, such as a map
+or a string, throws `IllegalArgumentException` before anything is written. A
type that a Jackson
+serializer of its own writes is not filtered, and a warning names it, as does
an `Iterable` that is not
+a `Collection`, whose elements are not walked before writing in case it can be
iterated only once. With
+`grails.web.rendering.json.spring: true`, `respond x, includes: [...]` without
a configuration name
+applies projections the same way; otherwise it uses the legacy converter.
+Registered configurations are immutable: configure them inside the
registration callback, and
+register the name again to replace a configuration rather than mutating a
retained reference.
+
+For direct serialization, inject the same registry and use
+`namedJsonConfigurationRegistry.writeValueAsString('deep', book)` or
+`namedJsonConfigurationRegistry.writeValue('deep', writer, book)`. These
explicit operations replace the
+thread-local scope established by `JSON.use('deep')`. The legacy
`JSON.createNamedConfig(...)` and `JSON.use(...)`
+APIs, along with `JSON.getNamedConfig(...)` and
`JSON.withDefaultConfiguration(...)`, are deprecated
+and scheduled for removal in Grails 11 in favor of the request-safe Jackson
registry. This timetable
+does not deprecate `JSON.registerObjectMarshaller(...)`.
+
+==== 5. Rendering Single Values as JSON
+
+In Grails 8, rendering a single enum value with `render(Number.ONE as JSON)`
threw a `ConverterException`.
+It now produces the JSON string `"ONE"`. The default enum marshaller uses the
enum name, including when the
+value is nested in an object or array.
+
+The legacy JSON converter now accepts scalar root values: strings (including
GStrings), numbers,
+booleans, `null`, and enum names. Use `new JSON(value)` for scalar values,
with or without
+`grails.converters.json.pretty.print=true`. Groovy's built-in casts for
strings and numbers do not
+support `value as JSON`; construct the converter explicitly for those values.
+Non-null scalar `respond` responses use the same rendering behavior. Rendering
a scalar converter
+no longer throws a `ConverterException` merely because the root is a scalar.
+
+==== 6. Opt Into Jackson JSON Request Binding
+
+Grails 9 retains `JsonSlurper` request binding by default and logs a startup
deprecation notice. As
+in Grails 8, an application `JsonSlurper` bean, for example one using
`JsonParserType.LAX`, replaces
+the default parser on that path; once Jackson parsing is enabled, configure it
through
+`JsonMapperBuilderCustomizer` instead.
+Set `grails.databinding.json.jackson: true` to parse binding bodies with
Boot's configured Jackson
+`JsonMapper`. Jackson becomes the default in Grails 10 and legacy parsing is
removed in Grails 11.
+Mapper features installed through `JsonMapperBuilderCustomizer` then apply
consistently
+to request parsing as well as response rendering. This includes registered
Jackson modules and parser
+features such as support for an application-specific input format.
+
+Both parsers report malformed bodies as binding errors, but Jackson rejects
additional inputs that
+the legacy parser accepted: trailing content after a document, trailing
commas, leading-zero numbers,
+unescaped tabs inside strings, trailing comments, nesting beyond its
configured depth limit, and
+integers beyond its configured number-length limit. Review clients before
enabling the new parser;
+configure intentional exceptions through `JsonMapperBuilderCustomizer`.
+
+With Jackson enabled, ordinary untyped JSON numbers keep the types the
previous parser produced. Values with a fractional part are read
+as `BigDecimal` rather than `Double`, so binding a request value such as
`1.15` to a `BigDecimal` property
+retains exactly the digits the request sent. Whole numbers bind as `Integer`,
`Long`, or `BigInteger`
+when too large for a `Long`; the legacy parser could overflow those large
integers.
+
+==== 7. REST Validation Errors Use RFC 9457 Problems
+
+With `grails.web.rendering.json.spring: true`, `respond()` renders Spring
`Errors` as
+`application/problem+json` instead of the legacy `{"errors": [...]}` body.
Grails 9 retains the legacy
+body by default; this follows the same Grails 10 default flip and Grails 11
removal schedule as JSON
+rendering. Grails uses the public `ValidationProblemDetailFactory` bean to
+adapt the errors to Spring Framework's RFC 9457 `ProblemDetail` type. The
resulting response has status
+422, the title `Validation failed`, and
+an `errors` extension containing stable maps with the object name, optional
field name, message, and error
+codes:
+
+[source,groovy]
+----
+import grails.rest.render.errors.ValidationProblemDetailFactory
+
+class BookController {
+ ValidationProblemDetailFactory validationProblemDetailFactory
+
+ def save(Book book) {
+ if (book.hasErrors()) {
+ respond validationProblemDetailFactory.create(book.errors)
+ return
+ }
+ // save the valid book
+ }
+}
+----
+
+Each message is resolved through the application's `MessageSource` for the
current locale, so
+`messages.properties` entries and Spring Boot's `spring.messages.*` settings
apply to validation
+responses as they do elsewhere.
+
+Applications can also inject the factory when they need to create or customize
the problem explicitly.
+When `respond` receives a `ProblemDetail`, it sets the HTTP status from the
problem, selects
+`application/problem+json`, and fills in an absent `instance` with the current
resource path. A
+`ProblemDetail` is always written through Spring's message converters, whatever
+`grails.web.rendering.json.spring` is set to, so the example above answers with
+`application/problem+json` in Grails 9 as well.
+Rejected field values are excluded by default because they can contain
passwords, tokens, or other
+sensitive request data. Applications that deliberately need those values can
define their own
+`new ValidationProblemDetailFactory(true)` bean after reviewing that exposure
risk.
+
+==== 8. HAL Rendering
+
+Existing Grails HAL JSON renderers remain available through explicit renderer
registration.
+Adding `spring-boot-starter-hateoas` alone does not register a Grails renderer
for Spring HATEOAS
+`EntityModel` values. Return those models from a Spring MVC controller or
provide an explicit Grails
+renderer for the desired media type.
+
+==== 9. Grails Format Resolution and Spring MVC Content Negotiation
+
+Grails format resolution -- `withFormat`, `respond`, and `response.format` --
applies the `grails.mime.*`
Review Comment:
One unconditional change in this path is not in the guide: the `Accept`
header is now parsed with Spring's `MediaType` parser and sorted by quality
value (`DefaultAcceptHeaderParser.parseRequestedMimeTypes`), with the previous
token split kept only for a value Spring rejects. Ordinary headers resolve the
same, so this is a note rather than a migration, but it belongs here so that an
application seeing a different `response.format` for an unusual header knows
where to look.
##########
grails-mimetypes/src/main/groovy/org/grails/web/mime/GrailsContentNegotiationStrategy.groovy:
##########
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.web.mime
+
+import java.util.regex.Pattern
+
+import groovy.transform.CompileStatic
+
+import jakarta.servlet.http.HttpServletRequest
+
+import org.springframework.http.MediaType
+import org.springframework.web.accept.ContentNegotiationStrategy
+import org.springframework.web.context.request.NativeWebRequest
+
+import grails.config.Config
+import grails.config.Settings
+import grails.web.http.HttpHeaders
+import grails.web.mime.MimeType
+import org.grails.web.util.GrailsApplicationAttributes
+import org.grails.web.util.WebUtils
+
+/**
+ * Adapts Grails' configured format aliases and compatibility rules to Spring
MVC content negotiation.
+ */
+@CompileStatic
+class GrailsContentNegotiationStrategy implements ContentNegotiationStrategy {
+
+ private final MimeType[] mimeTypes
+ private final boolean useAcceptHeader
+ private final boolean useAcceptHeaderXhr
+ private final Pattern disableForUserAgents
+
+ GrailsContentNegotiationStrategy(MimeType[] mimeTypes, Config config) {
+ this.mimeTypes = mimeTypes
+ this.useAcceptHeader =
config.getProperty(Settings.MIME_USE_ACCEPT_HEADER, Boolean, true)
+ this.useAcceptHeaderXhr = !config.getProperty(
+ Settings.MIME_DISABLE_ACCEPT_HEADER_FOR_USER_AGENTS_XHR,
+ Boolean,
+ false
+ )
+ this.disableForUserAgents = createDisabledUserAgentPattern(config)
+ }
+
+ @Override
+ List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) {
+ HttpServletRequest request =
webRequest.getNativeRequest(HttpServletRequest)
+ if (request == null) {
+ return [MediaType.ALL]
+ }
+ return resolveMimeTypes(request).collect { MimeType mimeType ->
+ SpringMediaTypeAdapter.toMediaType(mimeType)
+ }
+ }
+
+ /**
+ * @return the MIME types this strategy resolves against, as configured by
grails.mime.types
+ */
+ MimeType[] getConfiguredMimeTypes() {
+ return mimeTypes
+ }
+
+ MimeType[] resolveMimeTypes(HttpServletRequest request) {
+ String formatOverride
+ if (!WebUtils.isError(request)) {
+ formatOverride = request.getParameter('format')
+ }
+ if (!formatOverride) {
+ formatOverride =
request.getAttribute(GrailsApplicationAttributes.RESPONSE_FORMAT) as String
+ }
+ if (formatOverride) {
+ MimeType mimeType = mimeTypes.find { MimeType candidate ->
candidate.extension == formatOverride }
+ return [mimeType ?: mimeTypes[0]] as MimeType[]
Review Comment:
Verified at 173e94f9b8. `getMimeTypesInternal` now calls
`resolveAcceptedMimeTypes`, which is the 8.0.x parse (Accept header under the
user-agent rules, the GRAILS-8341 `all` fallback) with no format applied, and
`getMimeTypeForRequest`/`getFormat` are unchanged from 8.0.x, so
`response.format` and the format-aware list still carry the override while
`response.mimeTypes` does not. `resolveMimeTypes` keeps the override for
Spring. The `WithFormatSpec` case runs the probe I had used and gets `html`,
and the two paragraphs are gone from the guide. Closing.
##########
grails-doc/src/en/guide/testing/unitTesting.adoc:
##########
@@ -20,3 +20,17 @@ under the License.
Unit testing are tests at the "unit" level. In other words you are testing
individual methods or blocks of code without consideration for surrounding
infrastructure or interactions with other classes. Unit tests are typically run
without the presence of physical resources that involve I/O such as databases,
socket connections or files. This is to ensure they run as quick as possible
since quick feedback is important.
The Grails Unit Testing Support provides support for writing concise
expressive units tests for Grails artifacts with simple, easy to use traits and
supporting classes. The use of Traits makes functionality woven into the unit
test class at compile time, which allows developers to explicitly view and
explore the code. In addition, the Grails Unit Testing Support makes testing
more IDE-friendly so developers can take advantage of auto-completion and click
through navigation.
+
+Web test traits such as `ControllerUnitTest` and `TagLibUnitTest`
automatically include
+`restResponder`, including when a spec overrides `getIncludePlugins()`. They
provide JSON conversion
Review Comment:
This paragraph is the only place an upgrading application learns that its
web unit tests now include `restResponder`, boot Jackson, build Spring's
message converters and add the XML beans when `grails-xml` is on the test
classpath. A spec that overrides `getIncludePlugins()` or asserts on the bean
set will notice the difference, so please add a short testing note to the
Grails 9 upgrade guide that points here.
##########
grails-doc/src/en/guide/upgrading/upgrading90x.adoc:
##########
@@ -46,3 +46,316 @@ The Groovy actor event bus delivers each event to its
subscribers in publication
Applications that use GPars directly, for example `GParsPool.withPool {
list.eachParallel { } }`, should move to the Groovy equivalents: the parallel
collection methods are available on every `Collection` without a pool, and
`groovy.concurrent.ParallelScope.withPool(n) { scope -> list.eachParallel { }
}` runs them on a pool of a chosen size. Once the deprecated modules are
removed, Grails will no longer bring GPars onto the classpath, so an
application that still needs it must declare `org.codehaus.gpars:gpars` itself.
NOTE: Groovy 6 marks the concurrency toolkit as incubating. The Grails modules
that build on it expose only the Grails `Promise` and `EventBus` contracts, so
changes to the Groovy API in later Groovy releases are absorbed by Grails
rather than by application code.
+
+==== 2. XML Web Support Moves to a Separate Module
+
+XML conversion, request-body binding, codecs, and REST renderers now live in
the optional
+`grails-xml` module. The web starter still includes it in Grails 9, preserving
existing XML endpoints.
+JSON-only applications can exclude `org.apache.grails:grails-xml` from their
starter dependency now.
+In Grails 10 the starter will stop including it and `@Resource` will default
to JSON only.
+Legacy XML conversion logs one deprecation warning on use and is scheduled for
removal in Grails 11.
+MIME type negotiation still recognizes `application/xml`, `text/xml`, Atom,
and other XML media
+types; recognizing a media type does not install an XML serializer.
+
+New REST controllers, RESTful controllers, resources, and scaffolded
controllers generated by the
+`rest-api` profile advertise JSON only. After adding `grails-xml`, add `xml`
to the artefact's
+`responseFormats` or `formats` declaration when that endpoint should negotiate
XML.
+
+Applications that expose XML endpoints or use `grails.converters.XML`, XML
object marshallers, XML data
+binding, or the XML and Atom renderers need this module. It is supplied
transitively by the Grails 9
+web starter; applications without that starter, and applications upgrading to
Grails 10, add it explicitly:
+
+[source,groovy]
+.build.gradle
+----
+dependencies {
+ implementation 'org.apache.grails:grails-xml'
+}
+----
+
+The module preserves the established Grails XML APIs and compatibility
marshalling for domain-shaped
+payloads, collections, maps, validation errors, named converter
configurations, and include/exclude
+projections.
+
+Set `grails.web.rendering.xml.spring: true` to write ordinary bean responses
with Spring Framework's `JacksonXmlHttpMessageConverter`,
+backed by the Spring Boot-managed Jackson XML mapper. As in a plain Spring
Boot application, that
+converter is registered only when Jackson's XML dataformat is on the
classpath, so applications that
+want it add the dependency themselves:
+
+[source,groovy]
+.build.gradle
+----
+dependencies {
+ runtimeOnly 'tools.jackson.dataformat:jackson-dataformat-xml'
+}
+----
+
+Grails 9 defaults this setting to `false`: adding the Jackson XML dependency
alone does not bypass
+registered XML marshallers. Enabling it explicitly selects Spring for ordinary
beans, even when a
+legacy marshaller is registered. Domain objects, collections, maps, validation
errors and projections
+continue to use the Grails XML converter; migrate these to DTOs or explicit
application renderers
+before legacy XML conversion is removed in Grails 11.
+
+Without the Jackson XML dependency, `grails-xml` still renders XML through the
Grails converter. Applications may also register
+their own Spring XML message converter or OXM/JAXB infrastructure through
public Spring configuration
+APIs.
+
+This change does not affect Grails plugin descriptors. Plugin metadata
continues to be generated and
+published in its existing descriptor format; `grails-xml` concerns application
HTTP payloads only.
+
+==== 3. Opt Into Spring JSON Conversion
+
+Grails 9 preserves the legacy `grails.converters.JSON` converter for
`respond()` by default and logs one
+deprecation warning when that path renders. Opt into Spring MVC's message
converters, backed by Boot's
+configured Jackson `JsonMapper`, with `grails.web.rendering.json.spring:
true`. Grails 10 will enable it
+by default; the legacy `respond()` path is scheduled for removal in Grails 11.
Jackson can render date
+formats, bean properties, circular references, and validation errors
differently, so review the JSON
+returned by `respond()` endpoints when you upgrade. Explicit `render value as
JSON` calls still use the
+legacy converter.
+
+The setting alone determines the default response path: registering a
marshaller does not implicitly
+change it. `false` retains the legacy converter (with the warning); `true`
uses Spring's converters
+even while legacy marshallers are registered. Migrate those representations to
Jackson serializers
+before enabling it. Legacy pretty-print settings do not configure Jackson; use
+`spring.jackson.serialization.indent-output` on the Spring path. Renderer
beans an application
+declares, such as a `JsonRenderer` or an `XmlRenderer` for one of its types,
follow the same settings
+and reach the same named configurations and message converters.
+
+[source,yaml]
+.application.yml
+----
+grails:
+ web:
+ rendering:
+ json:
+ spring: true
+----
+
+Grails selects the first configured MVC `HttpMessageConverter` that can write
the
+response type and negotiated JSON media type and advertises a JSON media type
for that class
+(`application/json`, `text/json`, or a `+json` subtype). Generic `*/*` string
and byte-array converters
+are skipped, so strings remain JSON strings and byte arrays use Jackson's
base64 JSON representation.
+The same media-family rule applies to XML conversion; a generic text converter
cannot strip the XML
+string element.
+
+The standard Jackson converter uses an isolated mapper derived from Boot's
configured `JsonMapper`
+for Grails domain compatibility. Persistent properties, identifiers,
association identifiers, and
+per-response domain projections follow Grails metadata. On this domain path,
Jackson property
+annotations (`@JsonIgnore`, `@JsonProperty`, `@JsonInclude`, `@JsonView`,
`@JsonFormat`), property
+mixins, naming strategies, and transient getters do not define the
representation. Register an
+explicit serializer or return a DTO when that property model is required.
Ordinary non-domain beans
+retain Jackson's property rules. Application-provided converter subclasses and
per-type mapper
+registrations keep their configured behavior and precedence.
+
+Association identifiers use their mapped identity property name (for example
`uuid`), rather than
+the legacy marshaller's fixed `id` key. A value that leads back to a domain
object still being
+written, such as an embedded value that points at its owner, is written as the
legacy converter
+writes it: `{"_ref": "..", "class": "..."}` by default, or as
+`grails.converters.json.circular.reference.behaviour` selects. With `IGNORE`,
the property is written
+as `null` where the legacy converter left its name without a value. A cycle
made only of non-domain beans
+follows Jackson's rules, as it does in a plain Spring MVC controller, and
fails the response.
+
+Boot's shared mapper is not given the Grails domain serializer, so plain
Spring `@RestController`
+responses retain Jackson's normal domain property model. Both mappers
serialize Groovy `GString`
+values as strings. Only the Grails mapper serializes Spring `Errors` as an
`errors` array containing
+object, field, message, and codes, without rejected values; Grails `respond
errors` adapts that array into an
+RFC 9457 problem response.
+
+Grails retains the legacy converter path when a renderer sets a legacy named
converter
+configuration, when a per-response `includes`/`excludes` projection is
requested and the selected
+converter is not Spring's standard Jackson converter, or when no configured
MVC converter can write
+the selected type and media type. Explicit application and plugin `Renderer`
beans continue to take
+precedence over the default renderer.
+
+Jackson conversion uses UTF-8 for intermediate bytes; the response writer
applies
+`grails.converters.encoding`, so non-UTF response encodings do not corrupt the
intermediate JSON.
+
+Controller unit tests use Boot's Jackson configuration and the same setting.
Supply a mapper or
+`JsonMapperBuilderCustomizer` from a nested Spring `@Configuration` class, so
Boot can see it before
+evaluating its conditional beans. A mapper registered by the deprecated
`doWithSpring` callback is too
+late to make Boot back off. Named JSON
+configurations work through `render` and `respond` in `ControllerUnitTest`.
The optional `grails-xml`
+beans are contributed by web test traits when present, without loading the XML
plugin's dependency
+graph. Web traits automatically include `restResponder`, even when a test
overrides
+`getIncludePlugins()`, and provide JSON conversion beans and Boot's mapper
configuration.
+They do not automatically load the controllers, URL mappings or i18n plugins;
link generation retains
+the curated unit-test setup. Plain `GrailsUnitTest` and `ServiceUnitTest` do
not load XML or the web
+plugin graph simply because that module is on the classpath.
+Web unit tests build Spring MVC's message converters from the test classpath
and configuration; see
+<<unitTesting,Unit Testing>>.
+
+==== 4. JSON Converter Customization Moves to Jackson
+
+The `grails.converters.JSON.registerObjectMarshaller(...)` API remains
supported for
+`render ... as JSON` and converter-based rendering. Registering a marshaller
does not change the
+`respond()` rendering setting. Marshaller registrations apply to its legacy
converter path, not to
+Spring's message converters. To customize the Spring path, configure Spring
Boot's `JsonMapper` so
+the same serializer is used by opted-in `respond`, Spring MVC message
conversion, and other
+Jackson-based integrations:
+
+[source,groovy]
+----
+import
org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer
+import tools.jackson.databind.module.SimpleModule
+
+@Bean
+JsonMapperBuilderCustomizer bookJsonCustomizer() {
+ { builder ->
+ builder.addModule(new SimpleModule()
+ .addSerializer(Book, new BookValueSerializer()))
+ } as JsonMapperBuilderCustomizer
+}
+----
+
+`BookValueSerializer` is an application-provided Jackson
`ValueSerializer<Book>`. Use a module when several
+serializers belong together, or register an individual serializer directly
through the builder customizer.
+
+For different JSON shapes selected by name, replace
`JSON.createNamedConfig(...)` with the injectable
+`NamedJsonConfigurationRegistry`. Each registration derives an isolated mapper
from Spring Boot's configured
+`JsonMapper`, so it retains application-wide Jackson settings without changing
the shared mapper:
+
+[source,groovy]
+----
+import grails.converters.json.NamedJsonConfigurationRegistry
+
+class BootStrap {
+ NamedJsonConfigurationRegistry namedJsonConfigurationRegistry
+
+ def init = { servletContext ->
+ namedJsonConfigurationRegistry.register('deep') {
+ it.serializer(Type, new TypeValueSerializer())
+ it.serializer(Category, new CategoryValueSerializer())
+ }
+ }
+}
+----
+
+Select the configuration per response with `respond`, or with the `json`
render argument:
+
+[source,groovy]
+----
+respond book, jsonConfiguration: 'deep'
+render json: book, jsonConfiguration: 'deep'
+// Without a name, use the default Jackson configuration:
+render json: [ok: true]
+----
+
+Both accept `includes` and `excludes`. A projection applies to the properties
of the type written,
+or of each element's type when a collection or array is written, unlike the
legacy converter, which
+projected only a single root object. It applies to domain objects and other
beans alike; objects of
+other types inside them are written in full. Properties are named by their
bean property names, and a JSON name given by a Jackson
+annotation or naming strategy matches as well. A value with no properties to
project, such as a map
+or a string, throws `IllegalArgumentException` before anything is written. A
type that a Jackson
+serializer of its own writes is not filtered, and a warning names it, as does
an `Iterable` that is not
+a `Collection`, whose elements are not walked before writing in case it can be
iterated only once. With
+`grails.web.rendering.json.spring: true`, `respond x, includes: [...]` without
a configuration name
+applies projections the same way; otherwise it uses the legacy converter.
+Registered configurations are immutable: configure them inside the
registration callback, and
+register the name again to replace a configuration rather than mutating a
retained reference.
+
+For direct serialization, inject the same registry and use
+`namedJsonConfigurationRegistry.writeValueAsString('deep', book)` or
+`namedJsonConfigurationRegistry.writeValue('deep', writer, book)`. These
explicit operations replace the
+thread-local scope established by `JSON.use('deep')`. The legacy
`JSON.createNamedConfig(...)` and `JSON.use(...)`
+APIs, along with `JSON.getNamedConfig(...)` and
`JSON.withDefaultConfiguration(...)`, are deprecated
+and scheduled for removal in Grails 11 in favor of the request-safe Jackson
registry. This timetable
+does not deprecate `JSON.registerObjectMarshaller(...)`.
+
+==== 5. Rendering Single Values as JSON
+
+In Grails 8, rendering a single enum value with `render(Number.ONE as JSON)`
threw a `ConverterException`.
+It now produces the JSON string `"ONE"`. The default enum marshaller uses the
enum name, including when the
+value is nested in an object or array.
+
+The legacy JSON converter now accepts scalar root values: strings (including
GStrings), numbers,
+booleans, `null`, and enum names. Use `new JSON(value)` for scalar values,
with or without
+`grails.converters.json.pretty.print=true`. Groovy's built-in casts for
strings and numbers do not
+support `value as JSON`; construct the converter explicitly for those values.
+Non-null scalar `respond` responses use the same rendering behavior. Rendering
a scalar converter
+no longer throws a `ConverterException` merely because the root is a scalar.
+
+==== 6. Opt Into Jackson JSON Request Binding
+
+Grails 9 retains `JsonSlurper` request binding by default and logs a startup
deprecation notice. As
+in Grails 8, an application `JsonSlurper` bean, for example one using
`JsonParserType.LAX`, replaces
+the default parser on that path; once Jackson parsing is enabled, configure it
through
+`JsonMapperBuilderCustomizer` instead.
+Set `grails.databinding.json.jackson: true` to parse binding bodies with
Boot's configured Jackson
+`JsonMapper`. Jackson becomes the default in Grails 10 and legacy parsing is
removed in Grails 11.
+Mapper features installed through `JsonMapperBuilderCustomizer` then apply
consistently
+to request parsing as well as response rendering. This includes registered
Jackson modules and parser
+features such as support for an application-specific input format.
+
+Both parsers report malformed bodies as binding errors, but Jackson rejects
additional inputs that
+the legacy parser accepted: trailing content after a document, trailing
commas, leading-zero numbers,
+unescaped tabs inside strings, trailing comments, nesting beyond its
configured depth limit, and
+integers beyond its configured number-length limit. Review clients before
enabling the new parser;
+configure intentional exceptions through `JsonMapperBuilderCustomizer`.
+
+With Jackson enabled, ordinary untyped JSON numbers keep the types the
previous parser produced. Values with a fractional part are read
+as `BigDecimal` rather than `Double`, so binding a request value such as
`1.15` to a `BigDecimal` property
+retains exactly the digits the request sent. Whole numbers bind as `Integer`,
`Long`, or `BigInteger`
+when too large for a `Long`; the legacy parser could overflow those large
integers.
+
+==== 7. REST Validation Errors Use RFC 9457 Problems
+
+With `grails.web.rendering.json.spring: true`, `respond()` renders Spring
`Errors` as
+`application/problem+json` instead of the legacy `{"errors": [...]}` body.
Grails 9 retains the legacy
+body by default; this follows the same Grails 10 default flip and Grails 11
removal schedule as JSON
+rendering. Grails uses the public `ValidationProblemDetailFactory` bean to
+adapt the errors to Spring Framework's RFC 9457 `ProblemDetail` type. The
resulting response has status
+422, the title `Validation failed`, and
+an `errors` extension containing stable maps with the object name, optional
field name, message, and error
+codes:
+
+[source,groovy]
+----
+import grails.rest.render.errors.ValidationProblemDetailFactory
+
+class BookController {
+ ValidationProblemDetailFactory validationProblemDetailFactory
+
+ def save(Book book) {
+ if (book.hasErrors()) {
+ respond validationProblemDetailFactory.create(book.errors)
+ return
+ }
+ // save the valid book
+ }
+}
+----
+
+Each message is resolved through the application's `MessageSource` for the
current locale, so
+`messages.properties` entries and Spring Boot's `spring.messages.*` settings
apply to validation
+responses as they do elsewhere.
+
+Applications can also inject the factory when they need to create or customize
the problem explicitly.
+When `respond` receives a `ProblemDetail`, it sets the HTTP status from the
problem, selects
+`application/problem+json`, and fills in an absent `instance` with the current
resource path. A
+`ProblemDetail` is always written through Spring's message converters, whatever
+`grails.web.rendering.json.spring` is set to, so the example above answers with
+`application/problem+json` in Grails 9 as well.
+Rejected field values are excluded by default because they can contain
passwords, tokens, or other
+sensitive request data. Applications that deliberately need those values can
define their own
+`new ValidationProblemDetailFactory(true)` bean after reviewing that exposure
risk.
+
+==== 8. HAL Rendering
+
+Existing Grails HAL JSON renderers remain available through explicit renderer
registration.
+Adding `spring-boot-starter-hateoas` alone does not register a Grails renderer
for Spring HATEOAS
+`EntityModel` values. Return those models from a Spring MVC controller or
provide an explicit Grails
+renderer for the desired media type.
+
+==== 9. Grails Format Resolution and Spring MVC Content Negotiation
+
+Grails format resolution -- `withFormat`, `respond`, and `response.format` --
applies the `grails.mime.*`
+rules. Spring MVC keeps its own content negotiation: an `@RestController`, an
actuator endpoint or any other
+Spring handler continues to match `produces` against the `Accept` header
exactly as in a plain Spring
+Boot application, and still answers `406 Not Acceptable` for a media type it
cannot produce. The
+`spring.mvc.contentnegotiation.*` properties therefore continue to work.
+
+The data-format extensions `json`, `xml`, `hal`, `atom`, `rss`, `csv` and
`text` declared in
+`grails.mime.types` are additionally registered with Spring as format aliases.
Browser formats such
+as `html` and `js` are not registered, preserving Spring's
reflected-file-download protection.
+Application media types are preferred for aliases with multiple names, and
Boot's
+`spring.mvc.contentnegotiation.media-types.*` overrides these defaults.
Invalid names are skipped.
Review Comment:
Please say that these aliases take effect only when an application enables
Spring's parameter or extension negotiation
(`spring.mvc.contentnegotiation.favor-parameter=true`). Boot leaves both off,
so `?format=json` does not start steering Spring MVC endpoints on upgrade; as
written, a reader can expect it to.
##########
grails-web-databinding/src/main/groovy/org/grails/web/databinding/bindingsource/JsonDataBindingSourceCreator.groovy:
##########
@@ -44,12 +55,58 @@ import org.grails.web.json.JSONObject
* @see org.grails.databinding.bindingsource.DataBindingSourceCreator
*/
@CompileStatic
+@Slf4j
class JsonDataBindingSourceCreator extends
AbstractRequestBodyDataBindingSourceCreator {
private static final Pattern INDEX_PATTERN = ~/^(\S+)\[(\d+)\]$/
+ // Resolved when a request body is first parsed rather than injected.
Injecting it pulls
+ // Jackson's auto-configuration into this bean's graph, and
MimeTypesConfiguration depends on
+ // this creator, so Boot's mapper would be built before GORM has
initialized.
+ @Autowired(required = false)
+ ObjectProvider<JsonMapper> jsonMapperProvider
+
@Autowired(required = false)
- JsonSlurper jsonSlurper = new JsonSlurper()
+ ApplicationContext applicationContext
+
+ @Value('${grails.databinding.json.jackson:false}')
+ boolean useJackson = false
+
+ @PostConstruct
Review Comment:
Verified at 4efb6473e8. One static flag shared by the three creators, reset
through a preserved `ShutdownOperations` entry so the next application context
logs again; `@Autowired(required = false) JsonSlurper jsonSlurper = new
JsonSlurper()` is back and `parse()` uses it. The spec checks one notice across
the JSON, HAL and JSON API creators and the LAX bean parsing `{name: Grails}`;
section 6 names the bean. Closing.
##########
grails-converters/src/main/groovy/org/grails/web/converters/jackson/JsonProjection.java:
##########
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.grails.web.converters.jackson;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import tools.jackson.databind.BeanDescription;
+import tools.jackson.databind.JacksonModule;
+import tools.jackson.databind.SerializationConfig;
+import tools.jackson.databind.introspect.BeanPropertyDefinition;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.module.SimpleModule;
+import tools.jackson.databind.ser.BeanPropertyWriter;
+import tools.jackson.databind.ser.ValueSerializerModifier;
+
+import org.springframework.beans.BeanUtils;
+
+/**
+ * A per-write {@code includes}/{@code excludes} projection, as the {@code
render} and {@code respond}
+ * arguments request it.
+ *
+ * <p>As with the legacy converter, it applies to the properties of objects of
the written value's
+ * type, or of its elements' types when the value is a collection or an array.
It applies to domain
+ * objects and to beans Jackson writes property by property. Values with no
properties to project,
+ * such as maps and strings, are rejected before anything is written.</p>
+ *
+ * @since 9.0
+ */
+public final class JsonProjection {
+
+ /** Writer attribute holding the projection of one write. */
+ public static final String ATTRIBUTE = JsonProjection.class.getName();
+
+ private static final Logger LOG =
LoggerFactory.getLogger(JsonProjection.class);
+
+ private final List<String> includes;
+ private final List<String> excludes;
+ private final Set<Class<?>> types;
+ private final Set<Class<?>> applied = ConcurrentHashMap.newKeySet();
+
+ private JsonProjection(List<String> includes, List<String> excludes,
Set<Class<?>> types) {
+ this.includes = includes;
+ this.excludes = excludes;
+ this.types = types;
+ }
+
+ /**
+ * @param value the value to be written
+ * @param includes property names to include, or null for all
+ * @param excludes property names to exclude, or null for none
+ * @return the projection, or null if neither list names a property
+ * @throws IllegalArgumentException if the value, or an element of it, has
no properties to project
+ */
+ public static JsonProjection of(Object value, List<String> includes,
List<String> excludes) {
+ boolean including = includes != null && !includes.isEmpty();
+ boolean excluding = excludes != null && !excludes.isEmpty();
+ if (!including && !excluding) {
+ return null;
+ }
+ Set<Class<?>> types = new LinkedHashSet<>();
+ if (value instanceof Iterable<?> elements) {
Review Comment:
Verified at eb2e6d63cc. Only `Collection`s and arrays are walked; any other
`Iterable` is recorded and `reportUnapplied` warns that its elements were not
projected, and the spec writes a single-pass iterable and gets its element
back. Closing.
##########
grails-doc/src/en/guide/upgrading/upgrading90x.adoc:
##########
@@ -0,0 +1,333 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+////
+
+=== Upgrade Instructions for Grails and Related Dependencies
+
+This guide outlines the changes to review when you upgrade a Grails project
from Grails 8 to Grails 9.
+
+==== 1. XML Web Support Moves to a Separate Module
+
+XML conversion, request-body binding, codecs, and REST renderers now live in
the optional
+`grails-xml` module. The web starter still includes it in Grails 9, preserving
existing XML endpoints.
+JSON-only applications can exclude `org.apache.grails:grails-xml` from their
starter dependency now.
+In Grails 10 the starter will stop including it and `@Resource` will default
to JSON only.
+Legacy XML conversion logs one deprecation warning on use and is scheduled for
removal in Grails 11.
+MIME type negotiation still recognizes `application/xml`, `text/xml`, Atom,
and other XML media
+types; recognizing a media type does not install an XML serializer.
+
+New REST controllers, RESTful controllers, resources, and scaffolded
controllers generated by the
+`rest-api` profile advertise JSON only. After adding `grails-xml`, add `xml`
to the artefact's
+`responseFormats` or `formats` declaration when that endpoint should negotiate
XML.
+
+Applications that expose XML endpoints or use `grails.converters.XML`, XML
object marshallers, XML data
+binding, or the XML and Atom renderers need this module. It is supplied
transitively by the Grails 9
+web starter; applications without that starter, and applications upgrading to
Grails 10, add it explicitly:
+
+[source,groovy]
+.build.gradle
+----
+dependencies {
+ implementation 'org.apache.grails:grails-xml'
+}
+----
+
+The module preserves the established Grails XML APIs and compatibility
marshalling for domain-shaped
+payloads, collections, maps, validation errors, named converter
configurations, and include/exclude
+projections.
+
+Set `grails.web.rendering.xml.spring: true` to write ordinary bean responses
with Spring Framework's `JacksonXmlHttpMessageConverter`,
+backed by the Spring Boot-managed Jackson XML mapper. As in a plain Spring
Boot application, that
+converter is registered only when Jackson's XML dataformat is on the
classpath, so applications that
+want it add the dependency themselves:
+
+[source,groovy]
+.build.gradle
+----
+dependencies {
+ runtimeOnly 'tools.jackson.dataformat:jackson-dataformat-xml'
+}
+----
+
+Grails 9 defaults this setting to `false`: adding the Jackson XML dependency
alone does not bypass
+registered XML marshallers. Enabling it explicitly selects Spring for ordinary
beans, even when a
+legacy marshaller is registered. Domain objects, collections, maps, validation
errors and projections
+continue to use the Grails XML converter; migrate these to DTOs or explicit
application renderers
+before legacy XML conversion is removed in Grails 11.
+
+Without the Jackson XML dependency, `grails-xml` still renders XML through the
Grails converter. Applications may also register
+their own Spring XML message converter or OXM/JAXB infrastructure through
public Spring configuration
+APIs.
+
+This change does not affect Grails plugin descriptors. Plugin metadata
continues to be generated and
+published in its existing descriptor format; `grails-xml` concerns application
HTTP payloads only.
+
+==== 2. Opt Into Spring JSON Conversion
+
+Grails 9 preserves the legacy `grails.converters.JSON` converter for
`respond()` by default and logs one
+deprecation warning when that path renders. Opt into Spring MVC's message
converters, backed by Boot's
+configured Jackson `JsonMapper`, with `grails.web.rendering.json.spring:
true`. Grails 10 will enable it
+by default; the legacy `respond()` path is scheduled for removal in Grails 11.
Jackson can render date
+formats, bean properties, circular references, and validation errors
differently, so review the JSON
+returned by `respond()` endpoints when you upgrade. Explicit `render value as
JSON` calls still use the
+legacy converter.
+
+The setting alone determines the default response path: registering a
marshaller does not implicitly
+change it. `false` retains the legacy converter (with the warning); `true`
uses Spring's converters
+even while legacy marshallers are registered. Migrate those representations to
Jackson serializers
+before enabling it. Legacy pretty-print settings do not configure Jackson; use
+`spring.jackson.serialization.indent-output` on the Spring path.
+
+[source,yaml]
+.application.yml
+----
+grails:
+ web:
+ rendering:
+ json:
+ spring: true
+----
+
+Grails selects the first configured MVC `HttpMessageConverter` that can write
the
+response type and negotiated JSON media type and advertises a JSON media type
for that class
+(`application/json`, `text/json`, or a `+json` subtype). Generic `*/*` string
and byte-array converters
+are skipped, so strings remain JSON strings and byte arrays use Jackson's
base64 JSON representation.
+The same media-family rule applies to XML conversion; a generic text converter
cannot strip the XML
+string element.
+
+The standard Jackson converter uses an isolated mapper derived from Boot's
configured `JsonMapper`
+for Grails domain compatibility. Persistent properties, identifiers,
association identifiers, and
+per-response domain projections follow Grails metadata. On this domain path,
Jackson property
+annotations (`@JsonIgnore`, `@JsonProperty`, `@JsonInclude`, `@JsonView`,
`@JsonFormat`), property
+mixins, naming strategies, and transient getters do not define the
representation. Register an
+explicit serializer or return a DTO when that property model is required.
Ordinary non-domain beans
+retain Jackson's property rules. Application-provided converter subclasses and
per-type mapper
+registrations keep their configured behavior and precedence.
+
+Association identifiers use their mapped identity property name (for example
`uuid`), rather than
+the legacy marshaller's fixed `id` key. A value that leads back to a domain
object still being
+written, such as an embedded value that points at its owner, is written as the
legacy converter
+writes it: `{"_ref": "..", "class": "..."}` by default, or as
+`grails.converters.json.circular.reference.behaviour` selects. A cycle made
only of non-domain beans
+follows Jackson's rules, as it does in a plain Spring MVC controller, and
fails the response.
+
+Boot's shared mapper is not given the Grails domain serializer, so plain
Spring `@RestController`
+responses retain Jackson's normal domain property model. Both mappers
serialize Groovy `GString`
+values as strings. Only the Grails mapper serializes Spring `Errors` as an
`errors` array containing
+object, field, message, and codes, without rejected values; Grails `respond
errors` adapts that array into an
+RFC 9457 problem response.
+
+Grails retains the legacy converter path when a renderer sets a legacy named
converter
+configuration, when a per-response `includes`/`excludes` projection is
requested and the selected
+converter is not Spring's standard Jackson converter, or when no configured
MVC converter can write
+the selected type and media type. Explicit application and plugin `Renderer`
beans continue to take
+precedence over the default renderer.
+
+Jackson conversion uses UTF-8 for intermediate bytes; the response writer
applies
+`grails.converters.encoding`, so non-UTF response encodings do not corrupt the
intermediate JSON.
+
+Controller unit tests use Boot's Jackson configuration and the same setting.
Supply a mapper or
+`JsonMapperBuilderCustomizer` from a nested Spring `@Configuration` class, so
Boot can see it before
+evaluating its conditional beans. A mapper registered by the deprecated
`doWithSpring` callback is too
+late to make Boot back off. Named JSON
+configurations work through `render` and `respond` in `ControllerUnitTest`.
The optional `grails-xml`
+beans are contributed by web test traits when present, without loading the XML
plugin's dependency
+graph. Web traits automatically include `restResponder`, even when a test
overrides
+`getIncludePlugins()`, and provide JSON conversion beans and Boot's mapper
configuration.
+They do not automatically load the controllers, URL mappings or i18n plugins;
link generation retains
+the curated unit-test setup. Plain `GrailsUnitTest` and `ServiceUnitTest` do
not load XML or the web
+plugin graph simply because that module is on the classpath.
+Web unit tests build Spring MVC's message converters from the test classpath
and configuration; see
+<<unitTesting,Unit Testing>>.
+
+==== 3. JSON Converter Customization Moves to Jackson
+
+The `grails.converters.JSON.registerObjectMarshaller(...)` API remains
supported for
+`render ... as JSON` and converter-based rendering. Registering a marshaller
does not change the
+`respond()` rendering setting. Marshaller registrations apply to its legacy
converter path, not to
+Spring's message converters. To customize the Spring path, configure Spring
Boot's `JsonMapper` so
+the same serializer is used by opted-in `respond`, Spring MVC message
conversion, and other
+Jackson-based integrations:
+
+[source,groovy]
+----
+import
org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer
+import tools.jackson.databind.module.SimpleModule
+
+@Bean
+JsonMapperBuilderCustomizer bookJsonCustomizer() {
+ { builder ->
+ builder.addModule(new SimpleModule()
+ .addSerializer(Book, new BookValueSerializer()))
+ } as JsonMapperBuilderCustomizer
+}
+----
+
+`BookValueSerializer` is an application-provided Jackson
`ValueSerializer<Book>`. Use a module when several
+serializers belong together, or register an individual serializer directly
through the builder customizer.
+
+For different JSON shapes selected by name, replace
`JSON.createNamedConfig(...)` with the injectable
+`NamedJsonConfigurationRegistry`. Each registration derives an isolated mapper
from Spring Boot's configured
+`JsonMapper`, so it retains application-wide Jackson settings without changing
the shared mapper:
+
+[source,groovy]
+----
+import grails.converters.json.NamedJsonConfigurationRegistry
+
+class BootStrap {
+ NamedJsonConfigurationRegistry namedJsonConfigurationRegistry
+
+ def init = { servletContext ->
+ namedJsonConfigurationRegistry.register('deep') {
+ it.serializer(Type, new TypeValueSerializer())
+ it.serializer(Category, new CategoryValueSerializer())
+ }
+ }
+}
+----
+
+Select the configuration per response with `respond`, or with the `json`
render argument:
+
+[source,groovy]
+----
+respond book, jsonConfiguration: 'deep'
+render json: book, jsonConfiguration: 'deep'
+// Without a name, use the default Jackson configuration:
+render json: [ok: true]
+----
+
+Both accept `includes` and `excludes`. As with the legacy converter, a
projection applies to the
Review Comment:
Verified at eb2e6d63cc. The guide (section 4 after the merge) and the
registry Javadoc read "unlike the legacy converter, which projected only a
single root object", and the PR description now says `jsonConfiguration:`
selects only registry configurations. Closing.
##########
grails-doc/src/en/guide/upgrading/upgrading90x.adoc:
##########
@@ -0,0 +1,333 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+////
+
+=== Upgrade Instructions for Grails and Related Dependencies
+
+This guide outlines the changes to review when you upgrade a Grails project
from Grails 8 to Grails 9.
+
+==== 1. XML Web Support Moves to a Separate Module
+
+XML conversion, request-body binding, codecs, and REST renderers now live in
the optional
+`grails-xml` module. The web starter still includes it in Grails 9, preserving
existing XML endpoints.
+JSON-only applications can exclude `org.apache.grails:grails-xml` from their
starter dependency now.
+In Grails 10 the starter will stop including it and `@Resource` will default
to JSON only.
+Legacy XML conversion logs one deprecation warning on use and is scheduled for
removal in Grails 11.
Review Comment:
Verified at ac485a7092. The four XML classes are `@Deprecated(since = '9.0',
forRemoval = true)` with "Scheduled for removal in Grails 11" in their Javadoc,
`XmlCompatibilitySpec` asserts it, `hal.adoc` and `vndError.adoc` say Grails
11, and the six pages now say which parts come from `grails-xml`. Closing.
##########
grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/DefaultRendererRegistry.groovy:
##########
@@ -125,6 +156,8 @@ class DefaultRendererRegistry extends
ClassAndMimeTypeRegistry<Renderer, Rendere
if (renderer instanceof ContainerRenderer) {
ContainerRenderer cr = (ContainerRenderer) renderer
addContainerRenderer(cr.componentType, cr)
+ } else if (renderer instanceof FallbackRenderer) {
Review Comment:
Verified at b355586f58. `addRenderer` runs every `DefaultJsonRenderer`
through `configureJsonRenderer`, which fills only what the bean left unset and
shares the one legacy warning. Beans arrive through the `@Autowired`
`setRenderers`, and Spring injects a class's `@Value`/`@Autowired` fields
before its methods, so the registry's setting, named renderer, customizer and
converters are already resolved when they are copied; a renderer added
programmatically after startup gets them the same way. `useSpringJson` being
`null` is safe because both checks compare against `Boolean.TRUE`.
`DefaultXmlRenderer` falls back to the
`ObjectProvider<SpringMessageConverters>`. The three specs cover the JSON bean
with `jsonConfiguration:`, the XML bean and the kept explicit `false`. Closing.
##########
grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy:
##########
@@ -108,20 +160,164 @@ class DefaultJsonRenderer<T> implements Renderer<T> {
* @param context
*/
protected void renderJson(T object, RenderContext context) {
+ String selectedConfiguration =
context.arguments?.get('jsonConfiguration')?.toString()
+ if (selectedConfiguration) {
+ if (namedJsonRenderer == null ||
!namedJsonRenderer.contains(selectedConfiguration)) {
+ throw new IllegalArgumentException("Named JSON configuration
[$selectedConfiguration] is not registered.")
+ }
+ namedJsonRenderer.render(selectedConfiguration, object,
context.writer,
+ context.includes, context.excludes)
+ return
+ }
+ if (object instanceof ProblemDetail || canUseSpringConverter(context))
{
+ Object springValue = object
+ if (object instanceof Errors) {
+ springValue = validationProblemDetailFactory.create((Errors)
object, errorsHttpStatus)
+ }
+ MediaType mediaType = springValue instanceof ProblemDetail ?
+ MediaType.parseMediaType(PROBLEM_JSON.name) :
+ MediaType.parseMediaType(resolveMimeType(context).name)
+ // Set the content type before writing: once the writer flushes,
the response is
+ // committed and a later content type change is silently discarded.
+ if (springValue instanceof ProblemDetail) {
+ ProblemDetail problem = (ProblemDetail) springValue
+ context.setStatus(HttpStatus.valueOf(problem.status))
Review Comment:
Verified at a4a5f1c45a. `HttpStatus.resolve` plus the direct servlet status
for a code without a constant, with the `IllegalArgumentException` for a
non-servlet context; the status is still set before the body is written.
`DefaultJsonRendererSpec` runs 422, 499, 299 and 599, and section 7 (section 6
before the merge) now says a `ProblemDetail` always takes the Spring path.
Closing.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]