jdaugherty commented on code in PR #16237:
URL: https://github.com/apache/grails-core/pull/16237#discussion_r4326893715


##########
grails-doc/src/en/guide/REST/hypermedia/vndError.adoc:
##########
@@ -41,30 +44,62 @@ Content-Type: application/json;charset=ISO-8859-1
 }
 ----
 
-If you wish to change the format to Vnd.Error then simply register 
`grails.rest.render.errors.VndErrorJsonRenderer` bean in 
`grails-app/conf/spring/resources.groovy`:
+With `grails.web.rendering.json.spring: true`, which is the default from 
Grails 10, `respond` sends the
+errors as an RFC 9457 problem instead, and new applications should use that 
response:

Review Comment:
   Please drop "and new applications should use that response". The Grails 9 
default is the `{"errors": [...]}` body, which this page now rightly shows 
first; the guide should describe the problem response as what 
`grails.web.rendering.json.spring` opts into, not steer new applications to it 
ahead of the flip.



##########
grails-xml/src/main/groovy/org/apache/grails/xml/aot/XmlRuntimeHints.java:
##########
@@ -0,0 +1,55 @@
+/*
+ *  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.apache.grails.xml.aot;
+
+import java.util.List;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.TypeReference;
+
+import grails.converters.XML;
+import 
org.grails.web.converters.configuration.XmlConvertersConfigurationInitializer;
+import org.grails.web.databinding.bindingsource.XmlDataBindingSourceCreator;
+
+/**
+ * Registers the XML converter that {@code encodeAsXML} creates by name, and 
the initializer of its configurations.
+ *
+ * <p>The {@code xml} encoder of {@code grails-converters} renders an object 
other than text with the XML converter of
+ * this module, which depends on {@code grails-converters}, so it finds the 
converter by its name and creates it with
+ * its constructor that takes the object. An ahead-of-time image keeps neither 
unless something asks for them, and
+ * without them {@code encodeAsXML} escapes an object as its string, as it 
does without this module. The converters'
+ * initializer, when code initializes the converters itself, creates the 
initializer of the XML converter by name the
+ * same way, and the data binding registry its default XML binding source 
creator.</p>
+ *
+ * @since 9.0
+ */
+public class XmlRuntimeHints implements RuntimeHintsRegistrar {
+
+    @Override
+    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
+        hints.reflection().registerType(XML.class,

Review Comment:
   Nit. e0328c6be0 meant to keep grails-xml's own uses of the deprecated 
converter free of removal warnings, but this one is still reported by javac 
(`warning: [removal] XML in grails.converters has been deprecated and marked 
for removal`); a `@SuppressWarnings("removal")` on the class, as on the 
marshallers, silences it.
   



##########
grails-xml/src/main/groovy/org/grails/plugins/web/rest/render/xml/ProblemXml.groovy:
##########
@@ -0,0 +1,413 @@
+/*
+ *  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.plugins.web.rest.render.xml
+
+import javax.xml.stream.XMLOutputFactory
+import javax.xml.stream.XMLStreamException
+import javax.xml.stream.XMLStreamWriter
+
+import groovy.transform.CompileStatic
+import groovy.transform.PackageScope
+
+import tools.jackson.core.JsonGenerator
+import tools.jackson.core.JsonParser
+import tools.jackson.core.JsonToken
+import tools.jackson.core.StreamWriteConstraints
+import tools.jackson.core.StreamWriteFeature
+import tools.jackson.core.util.JsonGeneratorDelegate
+import tools.jackson.databind.JacksonSerializable
+import tools.jackson.databind.ObjectMapper
+import tools.jackson.databind.SerializationContext
+import tools.jackson.databind.SerializationFeature
+import tools.jackson.databind.exc.MismatchedInputException
+import tools.jackson.databind.jsontype.TypeSerializer
+import tools.jackson.databind.util.RawValue
+import tools.jackson.databind.util.TokenBuffer
+
+import org.springframework.http.ProblemDetail
+
+/**
+ * Writes a problem in the XML format of RFC 9457, as Spring's Jackson XML 
converter writes it, for an application without
+ * {@code jackson-dataformat-xml}: a {@code problem} element in the {@code 
urn:ietf:rfc:7807} namespace, with the members
+ * of the problem, those of a subclass included, and its extension members, as 
a mapper with Spring's mix-in for problems
+ * writes them, every member it writes included, also one whose name it writes 
twice, the properties of a bean in no
+ * namespace and the entries of a map in that of the map, as Spring's 
converter writes them, but for the members of RFC
+ * 9457, by the getters Spring's mix-in names, and the extension members of 
the problem, or of a problem it holds, which
+ * are in the namespace of RFC 9457. It fails, as Spring's
+ * converters fail, for a problem nested deeper than the mapper's {@code 
StreamWriteConstraints} allow. A list is written as an element for each of its 
values, wrapped in an element of its name where
+ * it is the property of a bean, and a {@code null} as an {@code xsi:nil} 
element.
+ *
+ * @since 9.0
+ */
+@PackageScope
+@CompileStatic
+final class ProblemXml {

Review Comment:
   Non-blocking. This class and `ProblemBeans` exist only to write what 
Spring's `JacksonXmlHttpMessageConverter` would write when 
`jackson-dataformat-xml` is absent, so they drift the day Spring changes its 
XML. `XmlProblemDetailSpec` comparing against the real converter is the right 
guard; please name that spec in the Javadoc here, so whoever touches this next 
keeps the comparison, and consider whether Grails 10, where XML leaves the 
default classpath, should drop the fallback and answer 
`application/problem+json` as Spring MVC does.
   



##########
grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy:
##########
@@ -102,26 +193,282 @@ class DefaultJsonRenderer<T> implements Renderer<T> {
     }
 
     /**
-     * Subclasses should override to customize JSON response rendering
+     * The named JSON configuration {@code respond} selects, which must be 
registered: also for a problem, which Spring's
+     * message converters write whatever the configuration, and for a view, so 
that a name that is wrong is found
+     * whatever writes the response.
+     */
+    private String requireRegisteredConfiguration(RenderContext context) {
+        String selectedConfiguration = 
context.arguments?.get('jsonConfiguration')?.toString()
+        if (selectedConfiguration && (namedJsonRenderer == null || 
!namedJsonRenderer.contains(selectedConfiguration))) {
+            throw new IllegalArgumentException("Named JSON configuration 
[$selectedConfiguration] is not registered.")
+        }
+        selectedConfiguration
+    }
+
+    /**
+     * Subclasses should override to customize JSON response rendering. A 
problem is written as Spring MVC writes it,
+     * whatever the renderer's own settings, unless a subclass renders it 
itself.
      *
      * @param object
      * @param context
      */
     protected void renderJson(T object, RenderContext context) {
+        String selectedConfiguration = requireRegisteredConfiguration(context)
+        // validation errors are written as the problem they make, as a 
problem is
+        if (selectedConfiguration && !(object instanceof ProblemDetail || 
object instanceof Errors)) {
+            try {
+                namedJsonRenderer.render(selectedConfiguration, object, 
context.writer,
+                        getEffectiveIncludes(context), 
getEffectiveExcludes(context))
+            }
+            catch (JacksonException | IOException e) {
+                // as respond reports it without a configuration, as Spring 
MVC does
+                throw JacksonWriteFailures.reported(e)
+            }
+            return
+        }
+        String springFallbackReason = null
+        if (object instanceof ProblemDetail || canUseSpringConverter(context)) 
{
+            Object springValue = object
+            if (object instanceof Errors) {
+                springValue = (validationProblemDetailFactory ?: 
DEFAULT_PROBLEM_DETAIL_FACTORY)
+                        .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) {
+                if (object instanceof Errors) {
+                    ProblemDetailResponse.prepareBuilt((ProblemDetail) 
springValue, context)
+                }
+                else {
+                    ProblemDetailResponse.prepare((ProblemDetail) springValue, 
context)
+                }
+                
context.setContentType(GrailsWebUtil.getContentType(PROBLEM_JSON.name, 
encoding))
+            }
+            springFallbackReason = writeWithSpringConverter(springValue, 
mediaType, context)
+            if (springFallbackReason == null) {
+                return
+            }
+            if (object instanceof Errors) {
+                // No converter could write the problem; restore the 
negotiated type for the
+                // legacy converter path below.
+                
context.setContentType(GrailsWebUtil.getContentType(resolveMimeType(context).name,
 encoding))
+            }
+        }
+
+        reportLegacyFallback(object, springFallbackReason)
         JSON converter
+        // A jsonConfiguration argument names a configuration of the JSON 
mapper, which the legacy converter does not have
         if (namedConfiguration) {
             JSON.use(namedConfiguration) {
-                converter = object as JSON
+                converter = new JSON(object)
             }
         } else {
-            converter = object as JSON
+            converter = new JSON(object)
         }
         renderJson(converter, context)
     }
 
+    private void reportLegacyFallback(Object object, String 
springFallbackReason) {
+        if (object instanceof ProblemDetail && springFallbackReason != null) {
+            if (problemFallbackReported.compareAndSet(false, true)) {
+                log.warn("A ProblemDetail is written through Spring's message 
converters, but ${springFallbackReason}, " +
+                        'so the legacy JSON converter writes it as a bean.')
+            }
+            return
+        }
+        if (!legacyFallbackReported.compareAndSet(false, true)) {
+            return
+        }
+        if (useSpringJson == Boolean.TRUE) {
+            if (springFallbackReason != null || namedConfiguration) {
+                log.warn("Spring JSON rendering is enabled, but 
[${object?.getClass()?.name}] is written by the " +
+                        'legacy JSON converter, which is deprecated and will 
be removed in Grails 11, because ' +
+                        "${springFallbackReason ?: "this renderer uses the 
legacy named configuration [${namedConfiguration}]"}.")
+            }
+            return
+        }
+        log.warn('Legacy respond() JSON rendering is deprecated and will be 
removed in Grails 11. ' +
+                'Migrate to Jackson serializers and set 
grails.web.rendering.json.spring=true. ' +
+                'Spring JSON becomes the default in Grails 10.')
+    }
+
+    /**
+     * The includes the projection and the legacy converter apply. A subclass 
that carries its own
+     * includes returns them here, so both response paths honour them.
+     */
+    protected List<String> getEffectiveIncludes(RenderContext context) {
+        return context.includes
+    }
+
+    /** The excludes the projection and the legacy converter apply; see {@link 
#getEffectiveIncludes}. */
+    protected List<String> getEffectiveExcludes(RenderContext context) {
+        return context.excludes
+    }
+
+    private List<HttpMessageConverter<?>> resolveSpringHttpMessageConverters() 
{
+        return springHttpMessageConvertersSupplier?.get() ?: []
+    }
+
+    private boolean canUseSpringConverter(RenderContext context) {
+        if (useSpringJson == Boolean.TRUE && 
!resolveSpringHttpMessageConverters() &&
+                missingConvertersReported.compareAndSet(false, true)) {
+            log.warn('Spring JSON rendering is enabled but no MVC message 
converters are available; using legacy JSON.')
+        }
+        return useSpringJson == Boolean.TRUE && 
resolveSpringHttpMessageConverters() && !namedConfiguration
+    }
+
+    /**
+     * @return {@code null} once the value is written, otherwise why the 
Spring converters could not write it
+     */
+    private String writeWithSpringConverter(Object object, MediaType 
mediaType, RenderContext context) {
+        if (mediaType.type == 'text' && mediaType.subtype == 'json') {
+            mediaType = MediaType.APPLICATION_JSON
+        }
+        Class<?> objectType = object?.getClass() ?: Object
+        HttpMessageConverter<Object> converter = 
(HttpMessageConverter<Object>) resolveSpringHttpMessageConverters().find {
+            HttpMessageConverter<?> candidate -> 
candidate.canWrite(objectType, mediaType) &&
+                    candidate.getSupportedMediaTypes(objectType).any { 
MediaType supported ->
+                        supported.subtype == 'json' || 
supported.subtype.endsWith('+json')
+                    }
+        }
+        if (converter == null) {
+            return "no Spring message converter writes [${objectType.name}] as 
${mediaType}"
+        }
+        // A problem is written as Spring MVC writes it, which no projection 
changes
+        boolean problem = object instanceof ProblemDetail
+        List<String> includes = problem ? null : getEffectiveIncludes(context)
+        List<String> excludes = problem ? null : getEffectiveExcludes(context)
+
+        // Only adapt the standard converter. Application converter subclasses 
and per-type
+        // mappers retain their own serialization contract, and a problem is 
written by the converter
+        // itself, with the mapper Spring MVC writes it with, as the XML 
converter writes it.
+        boolean grailsMapper = !problem && grailsJsonMapperCustomizer != null 
&&
+                converter.getClass() == JacksonJsonHttpMessageConverter &&
+                !((JacksonJsonHttpMessageConverter) 
converter).getMappersForType(objectType)
+        if ((includes != null || excludes) && !grailsMapper) {
+            // Only the Grails mapper applies a projection; the legacy 
converter applies it otherwise.
+            return 'includes and excludes need the Grails JSON mapper, which 
the application\'s message converter replaces'
+        }
+        // Jackson only writes UTF encodings. Use UTF-8 for the intermediate 
byte stream;
+        // the servlet writer still applies the configured response encoding.
+        Charset charset = SpringMessageConverters.writeCharset(converter, 
encoding)
+        MediaType contentType = new MediaType(mediaType, charset)
+        if (grailsMapper) {
+            JacksonJsonHttpMessageConverter source = 
(JacksonJsonHttpMessageConverter) converter
+            JsonProjection projection = JsonProjection.of(object, includes, 
excludes)
+            writeWithGrailsMapper(object, 
grailsJsonMapperCustomizer.forGrails(source.mapper), projection,
+                    prefixOf(source, contentType), context.writer)
+            projection?.reportUnapplied()
+            return null
+        }
+        WriterOutputStream.writeThrough(context.writer, charset) { 
OutputStream body ->
+            converter.write(object, contentType, outputMessage(body))
+        }
+        return null
+    }
+
+    /**
+     * Writes a value other than a problem as Spring MVC's converter writes 
it, with the isolated Grails mapper: the
+     * converter's prefix is application configuration, and the mapper, which 
is derived from the converter's, writes the
+     * wrapper of a root value, and a value a serializer hands to {@link 
JsonGenerator#writePOJO(Object)} as the value
+     * itself. One write writes the whole value, so that an object a 
serializer writes with {@code writePOJO} is found
+     * when it repeats one being written. The writer is left unflushed, so 
that a value that fails part-way leaves the
+     * response uncommitted.
+     */
+    private static void writeWithGrailsMapper(Object value, JsonMapper mapper, 
JsonProjection projection, String prefix,
+                                              Writer output) {
+        ObjectWriter writer = 
mapper.writer().without(StreamWriteFeature.AUTO_CLOSE_TARGET)
+                .without(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)
+        if (projection != null) {
+            writer = writer.withAttribute(JsonProjection.ATTRIBUTE, projection)
+        }
+        try {
+            output.write(prefix)
+            writer.writeValue(output, value)
+        }
+        catch (JacksonException | IOException e) {
+            // the converter writes its prefix with its generator, which 
reports a failure of the writer as Jackson's
+            throw JacksonWriteFailures.reported(e)
+        }
+    }
+
+    /**
+     * What a converter writes before a value, such as a JSON prefix, which it 
has no accessor for: what it writes for a
+     * marker, less the JSON its mapper writes for the marker, which includes 
the wrapper of a root value.
+     */
+    private static String prefixOf(JacksonJsonHttpMessageConverter converter, 
MediaType contentType) {

Review Comment:
   Non-blocking. `prefixOf` writes the marker through the converter and again 
through its mapper on every `respond` that takes the Grails-mapper path, so 
each such response pays two extra serializations and a `ByteArrayOutputStream` 
to recover a prefix that is converter configuration. A cache keyed by the 
converter instance, as `DefaultXmlRenderer.problemMappers` already keeps per 
mapper, would probe once per converter and still honour `setJsonPrefix`. Fine 
as a follow-up.
   



##########
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:
   Verified at a30f1d48bc: section 12 says the aliases steer Spring MVC only 
with `spring.mvc.contentnegotiation.favor-parameter: true`, which Boot leaves 
off, and the `GrailsContentNegotiationStrategySpec` feature covers 
`?format=xml` with `Accept: application/json`.



##########
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:
   Verified at a30f1d48bc: section 13 lists `restResponder`, Boot's Jackson 
configuration, the message converters, the content negotiation manager and the 
`grails-xml` beans, and points to Unit Testing.



-- 
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]

Reply via email to