This is an automated email from the ASF dual-hosted git repository.

sbglasius pushed a commit to branch fix/issue_15228-respond-errors
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 91c8b395673ffd1b4444f8b8b8da778b3bc1c7e0
Author: Søren Berg Glasius <[email protected]>
AuthorDate: Mon Nov 17 16:44:28 2025 +0100

    fix: Re-implemented ContainerRenderer on AbstractJsonViewContainerRenderer
    
    * Re-enabled gson view resolver
    * `ContainerRenderer` extends `Renderer` fix to generics parameter
    * - consequence fix, that `Renderer` calls `render(T render, ...` changed 
to `render(Object render, ...` and thus contract changed back to pre Grails 
7.0.x
    * New test project to verify outcome.
---
 .../grails/rest/render/ContainerRenderer.groovy    |   2 +-
 .../main/groovy/grails/rest/render/Renderer.groovy |   2 +-
 .../rest/render/errors/VndErrorJsonRenderer.groovy |   3 +-
 .../rest/render/errors/VndErrorXmlRenderer.groovy  |   3 +-
 .../grails/rest/render/hal/HalJsonRenderer.groovy  |   2 +-
 .../render/util/AbstractLinkingRenderer.groovy     |   4 +-
 .../rest/render/json/DefaultJsonRenderer.groovy    |   2 +-
 grails-test-examples/issue-15228/build.gradle      |  48 ++++++++++
 .../issue-15228/grails-app/conf/application.yml    |  58 ++++++++++++
 .../issue-15228/grails-app/conf/logback.xml        |  39 ++++++++
 .../issue15228/app/AppController.groovy            |  33 ++++---
 .../controllers/issue15228/app/UrlMappings.groovy  |  21 ++---
 .../init/issue15228/app/Application.groovy         |  22 ++---
 .../grails-app/views/app/normalView.gson           |  11 +++
 .../grails-app/views/errors/_errors.gson           |  21 +++++
 .../_otherValidateableObject.gson                  |  11 +++
 .../issue11767/app/GsonViewRespondSpec.groovy      | 104 +++++++++++++++++++++
 .../issue15228/app/OtherValidateableObject.groovy  |  11 +++
 .../issue15228/app/ValidateableObject.groovy       |  11 +++
 .../groovy/functional/tests/BookSpec.groovy        |  32 +++----
 .../views/mvc/renderer/DefaultViewRenderer.groovy  |   2 +-
 .../AbstractJsonViewContainerRenderer.groovy       |  11 +--
 settings.gradle                                    |   2 +
 23 files changed, 381 insertions(+), 74 deletions(-)

diff --git 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
index 4f1c612f19..e9ef4b2a4c 100644
--- 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
+++ 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
@@ -24,7 +24,7 @@ package grails.rest.render
  * @author Graeme Rocher
  * @since 2.3
  */
-interface ContainerRenderer<C, T> extends Renderer<C> {
+interface ContainerRenderer<C, T> extends Renderer<T> {
 
     /**
      * @return The underlying type wrapped by the container. For example with 
List<Book>, this method would return Book
diff --git 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/Renderer.groovy 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/Renderer.groovy
index 39c4ee06b1..1b8a93470b 100644
--- a/grails-rest-transforms/src/main/groovy/grails/rest/render/Renderer.groovy
+++ b/grails-rest-transforms/src/main/groovy/grails/rest/render/Renderer.groovy
@@ -39,5 +39,5 @@ interface Renderer<T> extends MimeTypeProvider {
      * @param object The object to render
      * @param context The {@link RenderContext}
      */
-    void render(T object, RenderContext context)
+    void render(Object object, RenderContext context)
 }
diff --git 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorJsonRenderer.groovy
 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorJsonRenderer.groovy
index 4fcd5642e9..df0882056d 100644
--- 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorJsonRenderer.groovy
+++ 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorJsonRenderer.groovy
@@ -25,7 +25,6 @@ import groovy.transform.CompileStatic
 import org.springframework.http.HttpMethod
 import org.springframework.http.HttpStatus
 import org.springframework.validation.BeanPropertyBindingResult
-import org.springframework.validation.Errors
 import org.springframework.validation.ObjectError
 
 import grails.rest.render.RenderContext
@@ -48,7 +47,7 @@ class VndErrorJsonRenderer extends AbstractVndErrorRenderer {
     MimeType[] mimeTypes = [MIME_TYPE, MimeType.HAL_JSON, MimeType.JSON, 
MimeType.TEXT_JSON] as MimeType[]
 
     @Override
-    void render(Errors object, RenderContext context) {
+    void render(Object object, RenderContext context) {
         if (messageSource == null) throw new 
IllegalStateException('messageSource property null')
         if (object instanceof BeanPropertyBindingResult) {
 
diff --git 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorXmlRenderer.groovy
 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorXmlRenderer.groovy
index 1e5d9f52b8..d5516a0686 100644
--- 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorXmlRenderer.groovy
+++ 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorXmlRenderer.groovy
@@ -22,7 +22,6 @@ import groovy.transform.CompileStatic
 
 import org.springframework.http.HttpStatus
 import org.springframework.validation.BeanPropertyBindingResult
-import org.springframework.validation.Errors
 import org.springframework.validation.ObjectError
 
 import grails.rest.render.RenderContext
@@ -50,7 +49,7 @@ class VndErrorXmlRenderer extends AbstractVndErrorRenderer {
     MimeType[] mimeTypes = [MIME_TYPE, MimeType.HAL_XML, MimeType.XML, 
MimeType.TEXT_XML] as MimeType[]
 
     @Override
-    void render(Errors object, RenderContext context) {
+    void render(Object object, RenderContext context) {
         if (object instanceof BeanPropertyBindingResult) {
             def errors = object as BeanPropertyBindingResult
             
context.setContentType(GrailsWebUtil.getContentType(MIME_TYPE.name, encoding))
diff --git 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/hal/HalJsonRenderer.groovy
 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/hal/HalJsonRenderer.groovy
index 2b5ff38129..d8e5ffb969 100644
--- 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/hal/HalJsonRenderer.groovy
+++ 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/hal/HalJsonRenderer.groovy
@@ -108,7 +108,7 @@ class HalJsonRenderer<T> extends AbstractLinkingRenderer<T> 
{
     }
 
     @Override
-    void renderInternal(T object, RenderContext context) {
+    void renderInternal(Object object, RenderContext context) {
         final mimeType = context.acceptMimeType ?: mimeTypes[0]
         final responseWriter = context.writer
         Writer targetWriter = prettyPrint ? new StringWriter() : responseWriter
diff --git 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/util/AbstractLinkingRenderer.groovy
 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/util/AbstractLinkingRenderer.groovy
index 1428d23ae0..e78a82644f 100644
--- 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/util/AbstractLinkingRenderer.groovy
+++ 
b/grails-rest-transforms/src/main/groovy/grails/rest/render/util/AbstractLinkingRenderer.groovy
@@ -107,7 +107,7 @@ abstract class AbstractLinkingRenderer<T> extends 
AbstractIncludeExcludeRenderer
     }
 
     @Override
-    void render(T object, RenderContext renderContext) {
+    void render(Object object, RenderContext renderContext) {
         def mimeType = renderContext.acceptMimeType ?: getMimeTypes()[0]
         def contentType = GrailsWebUtil.getContentType(mimeType.name, encoding)
         renderContext.setContentType(contentType)
@@ -123,7 +123,7 @@ abstract class AbstractLinkingRenderer<T> extends 
AbstractIncludeExcludeRenderer
         }
     }
 
-    abstract void renderInternal(T object, RenderContext context)
+    abstract void renderInternal(Object object, RenderContext context)
 
     protected boolean isDomainResource(Class clazz) {
         if (mappingContext != null) {
diff --git 
a/grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy
 
b/grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy
index 429f75c515..562dbc878b 100644
--- 
a/grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy
+++ 
b/grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy
@@ -76,7 +76,7 @@ class DefaultJsonRenderer<T> implements Renderer<T> {
     }
 
     @Override
-    void render(T object, RenderContext context) {
+    void render(Object object, RenderContext context) {
         final mimeType = context.acceptMimeType ?: MimeType.JSON
         context.setContentType(GrailsWebUtil.getContentType(mimeType.name, 
encoding))
         def viewName = context.viewName ?: context.actionName
diff --git a/grails-test-examples/issue-15228/build.gradle 
b/grails-test-examples/issue-15228/build.gradle
new file mode 100644
index 0000000000..8d586c75a6
--- /dev/null
+++ b/grails-test-examples/issue-15228/build.gradle
@@ -0,0 +1,48 @@
+/*
+ *  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.
+ */
+
+version = '0.1'
+group = 'issue11767.app'
+
+apply plugin: 'org.apache.grails.gradle.grails-web'
+
+dependencies {
+    implementation platform(project(':grails-bom'))
+
+    implementation 'org.springframework.boot:spring-boot-starter-logging'
+    implementation 'org.apache.grails:grails-dependencies-starter-web'
+
+    implementation 'org.apache.grails:grails-views-gson'
+    implementation 'org.apache.grails:grails-rest-transforms'
+
+    testAndDevelopmentOnly platform(project(':grails-bom'))
+
+    testImplementation 'org.apache.grails:grails-testing-support-views-gson'
+    testImplementation 'org.apache.grails.testing:grails-testing-support-core'
+
+    integrationTestImplementation 'com.fasterxml.jackson.core:jackson-databind'
+    integrationTestImplementation 
"io.micronaut:micronaut-http-client:$micronautHttpClientVersion"
+    integrationTestImplementation 
"io.micronaut:micronaut-jackson-databind:$micronautHttpClientVersion"
+}
+
+apply {
+    from 
rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle')
+    from rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
+    from 
rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle')
+}
diff --git a/grails-test-examples/issue-15228/grails-app/conf/application.yml 
b/grails-test-examples/issue-15228/grails-app/conf/application.yml
new file mode 100644
index 0000000000..af9d03596e
--- /dev/null
+++ b/grails-test-examples/issue-15228/grails-app/conf/application.yml
@@ -0,0 +1,58 @@
+# 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.
+
+info:
+  app:
+    name: '@info.app.name@'
+    version: '@info.app.version@'
+    grailsVersion: '@info.app.grailsVersion@'
+grails:
+  mime:
+    disable:
+      accept:
+        header:
+          userAgents:
+          - Gecko
+          - WebKit
+          - Presto
+          - Trident
+    types:
+      all: '*/*'
+      atom: application/atom+xml
+      css: text/css
+      csv: text/csv
+      form: application/x-www-form-urlencoded
+      html:
+      - text/html
+      - application/xhtml+xml
+      js: text/javascript
+      json:
+      - application/json
+      - text/json
+      multipartForm: multipart/form-data
+      pdf: application/pdf
+      rss: application/rss+xml
+      text: text/plain
+      hal:
+      - application/hal+json
+      - application/hal+xml
+      xml:
+      - text/xml
+      - application/xml
+micronaut:
+  http:
+    client:
+      readTimeout: PT10M
+      
\ No newline at end of file
diff --git a/grails-test-examples/issue-15228/grails-app/conf/logback.xml 
b/grails-test-examples/issue-15228/grails-app/conf/logback.xml
new file mode 100644
index 0000000000..d64fd5d712
--- /dev/null
+++ b/grails-test-examples/issue-15228/grails-app/conf/logback.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+     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.
+
+-->
+<configuration>
+
+    <conversionRule conversionWord="clr" 
class="org.springframework.boot.logging.logback.ColorConverter" />
+    <conversionRule conversionWord="wex" 
class="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"
 />
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <withJansi>true</withJansi>
+        <encoder>
+            <charset>UTF-8</charset>
+            <pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) 
%clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} 
%clr(:){faint} %m%n%wex</pattern>
+        </encoder>
+    </appender>
+
+    <root level="info">
+        <appender-ref ref="STDOUT" />
+    </root>
+
+</configuration>
diff --git 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
 
b/grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/AppController.groovy
similarity index 63%
copy from 
grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
copy to 
grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/AppController.groovy
index 4f1c612f19..d7d86b7092 100644
--- 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
+++ 
b/grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/AppController.groovy
@@ -16,18 +16,27 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package grails.rest.render
 
-/**
- * A container a renderer is a render that renders a container of objects 
(Example: List of Book instances)
- *
- * @author Graeme Rocher
- * @since 2.3
- */
-interface ContainerRenderer<C, T> extends Renderer<C> {
+package issue15228.app
+
+import org.springframework.http.HttpStatus
+
+import grails.artefact.Controller
+import grails.artefact.controller.RestResponder
 
-    /**
-     * @return The underlying type wrapped by the container. For example with 
List<Book>, this method would return Book
-     */
-    Class<T> getComponentType()
+class AppController implements Controller, RestResponder {
+
+    def normalView(ValidateableObject obj) {
+        respond(obj)
+    }
+
+    def typeView(OtherValidateableObject obj) {
+        respond(obj)
+    }
+
+    def errorView(ValidateableObject obj) {
+        respond(obj.errors, status: HttpStatus.UNPROCESSABLE_ENTITY)
+    }
+    
 }
+
diff --git 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
 
b/grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/UrlMappings.groovy
similarity index 66%
copy from 
grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
copy to 
grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/UrlMappings.groovy
index 4f1c612f19..34fe52fd3d 100644
--- 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
+++ 
b/grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/UrlMappings.groovy
@@ -16,18 +16,15 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package grails.rest.render
 
-/**
- * A container a renderer is a render that renders a container of objects 
(Example: List of Book instances)
- *
- * @author Graeme Rocher
- * @since 2.3
- */
-interface ContainerRenderer<C, T> extends Renderer<C> {
+package issue15228.app
 
-    /**
-     * @return The underlying type wrapped by the container. For example with 
List<Book>, this method would return Book
-     */
-    Class<T> getComponentType()
+class UrlMappings {
+    static mappings = {
+        "/$controller/$action?/$id?(.$format)?"{
+            constraints {
+                // apply constraints here
+            }
+        }
+    }
 }
diff --git 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
 
b/grails-test-examples/issue-15228/grails-app/init/issue15228/app/Application.groovy
similarity index 66%
copy from 
grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
copy to 
grails-test-examples/issue-15228/grails-app/init/issue15228/app/Application.groovy
index 4f1c612f19..cb27071abc 100644
--- 
a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy
+++ 
b/grails-test-examples/issue-15228/grails-app/init/issue15228/app/Application.groovy
@@ -16,18 +16,16 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package grails.rest.render
 
-/**
- * A container a renderer is a render that renders a container of objects 
(Example: List of Book instances)
- *
- * @author Graeme Rocher
- * @since 2.3
- */
-interface ContainerRenderer<C, T> extends Renderer<C> {
+package issue15228.app
+
+import grails.boot.GrailsApp
+import grails.boot.config.GrailsAutoConfiguration
+import groovy.transform.CompileStatic
 
-    /**
-     * @return The underlying type wrapped by the container. For example with 
List<Book>, this method would return Book
-     */
-    Class<T> getComponentType()
+@CompileStatic
+class Application extends GrailsAutoConfiguration {
+    static void main(String[] args) {
+        GrailsApp.run(Application, args)
+    }
 }
diff --git 
a/grails-test-examples/issue-15228/grails-app/views/app/normalView.gson 
b/grails-test-examples/issue-15228/grails-app/views/app/normalView.gson
new file mode 100644
index 0000000000..d15b5b2fbc
--- /dev/null
+++ b/grails-test-examples/issue-15228/grails-app/views/app/normalView.gson
@@ -0,0 +1,11 @@
+import issue15228.app.ValidateableObject
+
+model {
+    ValidateableObject validateableObject
+}
+
+json {
+    normal {
+        foo validateableObject.foo
+    }
+}
diff --git 
a/grails-test-examples/issue-15228/grails-app/views/errors/_errors.gson 
b/grails-test-examples/issue-15228/grails-app/views/errors/_errors.gson
new file mode 100644
index 0000000000..18ee4a5a3a
--- /dev/null
+++ b/grails-test-examples/issue-15228/grails-app/views/errors/_errors.gson
@@ -0,0 +1,21 @@
+import org.springframework.validation.*
+
+model {
+    Errors errors
+}
+
+json {
+    error {
+        if(errors.globalErrorCount == 1 && errors.fieldErrorCount == 0) {
+            message messageSource.getMessage(errors.globalError, locale)
+        } else {
+            errors(errors.allErrors) { ObjectError error ->
+                if(error instanceof FieldError) {
+                    field error.field
+                    rejectedValue error.rejectedValue
+                }
+                message messageSource.getMessage(error, locale)
+            }
+        }
+    }
+}
diff --git 
a/grails-test-examples/issue-15228/grails-app/views/otherValidateableObject/_otherValidateableObject.gson
 
b/grails-test-examples/issue-15228/grails-app/views/otherValidateableObject/_otherValidateableObject.gson
new file mode 100644
index 0000000000..480d00f8a3
--- /dev/null
+++ 
b/grails-test-examples/issue-15228/grails-app/views/otherValidateableObject/_otherValidateableObject.gson
@@ -0,0 +1,11 @@
+import issue15228.app.OtherValidateableObject
+
+model {
+    OtherValidateableObject otherValidateableObject
+}
+
+json {
+    type {
+        foo otherValidateableObject.foo
+    }
+}
diff --git 
a/grails-test-examples/issue-15228/src/integration-test/groovy/issue11767/app/GsonViewRespondSpec.groovy
 
b/grails-test-examples/issue-15228/src/integration-test/groovy/issue11767/app/GsonViewRespondSpec.groovy
new file mode 100644
index 0000000000..82d8adbea5
--- /dev/null
+++ 
b/grails-test-examples/issue-15228/src/integration-test/groovy/issue11767/app/GsonViewRespondSpec.groovy
@@ -0,0 +1,104 @@
+/*
+ *  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 issue11767.app
+
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.HttpStatus
+import io.micronaut.http.MediaType
+import io.micronaut.http.client.HttpClient
+import io.micronaut.http.client.exceptions.HttpClientResponseException
+import spock.lang.AutoCleanup
+import spock.lang.Shared
+import spock.lang.Specification
+
+import grails.testing.mixin.integration.Integration
+
+@Integration
+class GsonViewRespondSpec extends Specification {
+
+    @Shared
+    ObjectMapper objectMapper
+
+    def setupSpec() {
+        objectMapper = new ObjectMapper()
+    }
+
+    @Shared
+    @AutoCleanup
+    HttpClient httpClient
+
+    void setup() {
+        def baseUrl = "http://localhost:$serverPort";
+        httpClient = HttpClient.create(baseUrl.toURL())
+    }
+
+    void 'respond with Error gson view'() {
+        when: 'The app controller is visited on errorView'
+        def request = 
HttpRequest.GET('/app/errorView?foo=Too+Short').accept(MediaType.APPLICATION_JSON)
+        httpClient.toBlocking().exchange(request, String)
+
+        then:
+        def ex = thrown(HttpClientResponseException)
+        ex.status == HttpStatus.UNPROCESSABLE_ENTITY
+
+        and:
+        objectMapper.readTree(ex.response.body() as String) == 
objectMapper.readTree('''
+            {
+              "error": {
+                "errors": [
+                  {
+                    "field": "foo",
+                    "rejectedValue": "Too Short",
+                    "message": "Property [foo] of class [class 
issue15228.app.ValidateableObject] with value [Too Short] is less than the 
minimum size of [10]"
+                  }
+                ]
+              }
+            }''')
+    }
+
+    void 'respond with gson view from action name'() {
+        when: 'The app controller is visited on normalView'
+        def request = 
HttpRequest.GET('/app/normalView?foo=Testing+normal+view').accept(MediaType.APPLICATION_JSON)
+        def response = httpClient.toBlocking().exchange(request, String)
+
+        then:
+        objectMapper.readTree(response.body() as String) == 
objectMapper.readTree('''{
+              "normal": {
+                "foo": "Testing normal view"
+              } 
+           }''')
+    }
+
+    void 'respond with gson view from type'() {
+        when: 'The app controller is visited on typeView'
+        def request = 
HttpRequest.GET('/app/typeView?foo=Testing+type+view').accept(MediaType.APPLICATION_JSON)
+        def response = httpClient.toBlocking().exchange(request, String)
+
+        then:
+        objectMapper.readTree(response.body() as String) == 
objectMapper.readTree('''{
+              "type": {
+                "foo": "Testing type view"
+              } 
+           }''')
+    }
+
+}
diff --git 
a/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/OtherValidateableObject.groovy
 
b/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/OtherValidateableObject.groovy
new file mode 100644
index 0000000000..62e0788239
--- /dev/null
+++ 
b/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/OtherValidateableObject.groovy
@@ -0,0 +1,11 @@
+package issue15228.app
+
+import grails.validation.Validateable
+
+class OtherValidateableObject implements Validateable {
+    String foo
+    
+    static constraints = {
+        foo minSize: 10
+    }
+}
\ No newline at end of file
diff --git 
a/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/ValidateableObject.groovy
 
b/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/ValidateableObject.groovy
new file mode 100644
index 0000000000..16c918d148
--- /dev/null
+++ 
b/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/ValidateableObject.groovy
@@ -0,0 +1,11 @@
+package issue15228.app
+
+import grails.validation.Validateable
+
+class ValidateableObject implements Validateable {
+    String foo
+    
+    static constraints = {
+        foo minSize: 10
+    }
+}
\ No newline at end of file
diff --git 
a/grails-test-examples/views-functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy
 
b/grails-test-examples/views-functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy
index 5001b99117..626206fe86 100644
--- 
a/grails-test-examples/views-functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy
+++ 
b/grails-test-examples/views-functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy
@@ -34,13 +34,8 @@ import spock.lang.Shared
 @Integration(applicationClass = Application)
 class BookSpec extends HttpClientSpec {
 
-    @Shared
-    ObjectMapper objectMapper
-
-    def setupSpec() {
-        objectMapper = new ObjectMapper()
-    }
 
+    
     @RunOnce
     @BeforeEach
     void init() {
@@ -56,22 +51,17 @@ class BookSpec extends HttpClientSpec {
         HttpClientResponseException e = thrown()
         e.response.status == HttpStatus.UNPROCESSABLE_ENTITY
         e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent()
-        // This has changed somewhere along the way
-        // e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 
'application/vnd.error;charset=UTF-8'
-        // to ->
-        e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 
'application/json;charset=UTF-8'
-        objectMapper.readTree(e.response.body().toString()) == 
objectMapper.readTree('''
+        e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 
'application/vnd.error;charset=UTF-8'
+        objectMapper.readTree(e.response.body().toString()) == 
objectMapper.readTree("""
             {
-                "errors": [
-                    {
-                        "object": "functional.tests.Book",
-                        "field": "title",
-                        "rejected-value": null,
-                        "message": "Property [title] of class [class 
functional.tests.Book] cannot be null"
-                    }
-                ]
-            }
-        ''')
+              "message": "Property [title] of class [class 
functional.tests.Book] cannot be null",
+              "path": "/book/index",
+              "_links": {
+                "self": {
+                  "href": "$baseUrl/book/index"
+                }
+              }
+            }""")
     }
 
     void 'Test REST view rendering'() {
diff --git 
a/grails-views-core/src/main/groovy/grails/views/mvc/renderer/DefaultViewRenderer.groovy
 
b/grails-views-core/src/main/groovy/grails/views/mvc/renderer/DefaultViewRenderer.groovy
index 6543c82837..0e04501f44 100644
--- 
a/grails-views-core/src/main/groovy/grails/views/mvc/renderer/DefaultViewRenderer.groovy
+++ 
b/grails-views-core/src/main/groovy/grails/views/mvc/renderer/DefaultViewRenderer.groovy
@@ -64,7 +64,7 @@ abstract class DefaultViewRenderer<T> extends 
DefaultHtmlRenderer<T> {
     }
 
     @Override
-    void render(T object, RenderContext context) {
+    void render(Object object, RenderContext context) {
         def arguments = context.arguments
         def ct = arguments?.contentType
 
diff --git 
a/grails-views-gson/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy
 
b/grails-views-gson/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy
index d32b2c9ee6..384cf1c9a7 100644
--- 
a/grails-views-gson/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy
+++ 
b/grails-views-gson/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy
@@ -25,6 +25,7 @@ import groovy.transform.InheritConstructors
 import org.springframework.beans.factory.annotation.Autowired
 
 import grails.plugin.json.view.mvc.JsonViewResolver
+import grails.rest.render.ContainerRenderer
 import grails.rest.render.RenderContext
 import grails.util.GrailsNameUtils
 import grails.views.Views
@@ -39,13 +40,13 @@ import 
org.grails.plugins.web.rest.render.json.DefaultJsonRenderer
  */
 @CompileStatic
 @InheritConstructors
-abstract class AbstractJsonViewContainerRenderer<C,T> extends 
DefaultJsonRenderer<T> {
+abstract class AbstractJsonViewContainerRenderer<C,T> extends 
DefaultJsonRenderer<T> implements ContainerRenderer<C,T> {
 
     @Autowired
     JsonViewResolver jsonViewResolver
 
     @Override
-    void render(T object, RenderContext context) {
+    void render(Object object, RenderContext context) {
         if (jsonViewResolver != null) {
             String viewUri = 
"/${context.controllerName}/_${GrailsNameUtils.getPropertyName(targetType)}"
             def webRequest = ((ServletRenderContext) context).getWebRequest()
@@ -68,12 +69,10 @@ abstract class AbstractJsonViewContainerRenderer<C,T> 
extends DefaultJsonRendere
                 def request = webRequest.currentRequest
                 def response = webRequest.currentResponse
                 view.render(model, request, response)
-            }
-            else {
+            } else {
                 super.render(object, context)
             }
-        }
-        else {
+        } else {
             super.render(object, context)
         }
     }
diff --git a/settings.gradle b/settings.gradle
index 3e2a209b7d..196ca167c3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -406,6 +406,7 @@ include(
         'grails-test-examples-plugins-loadafter',
         'grails-test-examples-plugins-issue11005',
         'grails-test-examples-issue-11767',
+        'grails-test-examples-issue-15228',
         'grails-test-examples-plugins-exploded',
         'grails-test-examples-plugins-issue-11767',
         'grails-test-examples-cache',
@@ -438,6 +439,7 @@ 
project(':grails-test-examples-plugins-loadsecond').projectDir = file('grails-te
 project(':grails-test-examples-plugins-loadafter').projectDir = 
file('grails-test-examples/plugins/loadafter')
 project(':grails-test-examples-plugins-issue11005').projectDir = 
file('grails-test-examples/plugins/issue11005')
 project(':grails-test-examples-issue-11767').projectDir = 
file('grails-test-examples/issue-11767')
+project(':grails-test-examples-issue-15228').projectDir = 
file('grails-test-examples/issue-15228')
 project(':grails-test-examples-plugins-exploded').projectDir = 
file('grails-test-examples/plugins/exploded')
 project(':grails-test-examples-plugins-issue-11767').projectDir = 
file('grails-test-examples/plugins/issue-11767')
 project(':grails-test-examples-cache').projectDir = 
file('grails-test-examples/cache')

Reply via email to