MartinJesusDev opened a new issue, #16660:
URL: https://github.com/apache/grails-core/issues/16660
### Expected Behavior
The same `java.util.Date` rendered through `g.render` should use the JSON
Views generator
(`grails.views.json.generator.dateFormat`,
`grails.views.json.generator.timeZone`,
`grails.views.json.generator.locale`) regardless of its position in the
object graph: a direct bean
property, a nested bean property, or a property of an element of a
`List<DTO>`, a `DTO[]` or a raw list.
With the default configuration every date in the example below should be
`2025-10-08T07:48:46.407Z`. The JSON Views guide documents that "Values
rendered with `g.render` are
written the same way"
(`grails-doc/src/en/guide/theWebLayer/gson/jsonConfiguration.adoc`).
### Actual Behaviour
```groovy
class DatedItem { Date date }
class Holder {
Date direct
DatedItem item
List<DatedItem> items
DatedItem[] array
List<Object> rawItems
List<Date> dates
}
```
`grails-app/views/holder/show.gson`:
```groovy
model { Object object }
json g.render(object)
```
Output on the current 8.0.x sources with the default configuration (real
values from the failing
regression test):
```json
{
"direct" : "2025-10-08T07:48:46.407Z",
"item" : {"date": "2025-10-08T07:48:46.407Z"},
"items" : [{"date": "2025-10-08T07:48:46+0000"}],
"array" : [{"date": "2025-10-08T07:48:46+0000"}],
"rawItems": [{"date": "2025-10-08T07:48:46+0000"}],
"dates" : ["2025-10-08T07:48:46.407Z"]
}
```
The elements of `items`, `array` and `rawItems` are written by Groovy's
default generator
(`yyyy-MM-dd'T'HH:mm:ssZ`): the milliseconds are lost and the offset
representation differs. Accepting
`+0000` in a client does not recover the lost precision. JSON object key
order is irrelevant.
The same inconsistency affects three more rendering paths (all verified
failing before the prepared
fix, see "Source-level explanation"):
* `g.inline(object)` writes `"direct":"2025-10-08T07:48:46+0000"`.
* A child view that `inherits` a parent and does `json g.render(object)`
(inline rendering) writes
`{"foo":"bar","direct":"2025-10-08T07:48:46+0000"}` for a POGO and
`{"foo":"bar","name":"Fred","created":"2025-10-08T07:48:46+0000"}` for a
domain instance.
* `hal.embedded { item(object) { created object.created } }` writes the
embedded date as
`2025-10-08T07:48:46+0000` instead of `2025-10-08T07:48:46.407Z`.
### Steps To Reproduce
**A. Regression test against the Grails sources (8.0.x).** The prepared PR
adds this test to
`grails-views-gson`; it fails on the current sources and passes with the fix:
```groovy
class DateTimeHelperRenderingSpec extends Specification implements
JsonViewUnitTest {
void "g.render of a POGO writes a Date in a collection or array of DTOs
the same way as its direct property"() {
given:
def date = Date.from(Instant.parse('2025-10-08T07:48:46.407Z'))
def holder = new CollectionDateHolder(
direct: date,
item: new DatedItem(date: date),
items: [new DatedItem(date: date)],
array: [new DatedItem(date: date)] as DatedItem[],
rawItems: [new DatedItem(date: date)],
dates: [date] as Date[])
when:
def result = render('''
model { Object object }
json g.render(object)
''', [object: holder])
then:
result.json.direct == '2025-10-08T07:48:46.407Z'
result.json.item.date == result.json.direct
result.json.items*.date == [result.json.direct]
result.json.array*.date == [result.json.direct]
result.json.rawItems*.date == [result.json.direct]
result.json.dates == [result.json.direct]
}
}
```
Result before the fix (rest of the tests in the same run printed the
expected values):
```
result.json.items*.date == [result.json.direct]
| | | | | | |
| | | | | | [array:[[date:2025-10-08T07:48:46+0000]],
| | | | | | dates:[2025-10-08T07:48:46.407Z],
| | | | | | direct:2025-10-08T07:48:46.407Z,
| | | | | | item:[date:2025-10-08T07:48:46.407Z],
| | | | | | items:[[date:2025-10-08T07:48:46+0000]],
| | | | | |
rawItems:[[date:2025-10-08T07:48:46+0000]]]
| | | | | | false
| | | | | 2025-10-08T07:48:46.407Z
```
**B. Standalone reproducer against published artifacts (verified on
7.2.4).** Create a directory with
three files and run `gradle --no-daemon --no-parallel --max-workers=1
reproduce` with JDK 25 and
Gradle 9.1.0. No application, database, authentication or HTTP server is
required; it invokes the JSON
Views engine directly.
`settings.gradle`
```groovy
rootProject.name = 'json-view-generator-reproducer'
```
`build.gradle`
```groovy
repositories {
mavenCentral()
maven { url = uri('https://repo.grails.org/grails/restricted') }
}
configurations { probe }
dependencies {
probe platform('org.apache.grails:grails-bom:7.2.4')
probe 'org.apache.grails:grails-views-gson'
probe 'jakarta.servlet:jakarta.servlet-api'
}
tasks.register('reproduce', JavaExec) {
classpath = configurations.probe
mainClass = 'groovy.ui.GroovyMain'
args layout.projectDirectory.file('reproduce.groovy').asFile.absolutePath
}
```
`reproduce.groovy`
```groovy
package reproducer
import grails.plugin.json.view.JsonViewTemplateEngine
import groovy.json.JsonSlurper
import java.time.Instant
class Item { Date date }
class Envelope {
Date direct
Item nested
List<Item> items
}
Date date = Date.from(Instant.parse('2021-03-02T00:00:00.123Z'))
def envelope = new Envelope(direct: date, nested: new Item(date: date),
items: [new Item(date: date)])
def engine = new JsonViewTemplateEngine(getClass().classLoader)
def template = engine.createTemplate('import groovy.transform.Field\n@Field
Object object\njson g.render(object)')
def writer = new StringWriter()
template.make([object: envelope]).writeTo(writer)
String output = writer.toString()
println output
def result = new JsonSlurper().parseText(output)
assert result.direct == '2021-03-02T00:00:00.123Z'
assert result.nested.date == result.direct
assert result.items[0].date == result.direct : 'Collection element ignores
the Views generator and loses milliseconds'
```
Verified result on 7.2.4:
```json
{"direct":"2021-03-02T00:00:00.123Z","items":[{"date":"2021-03-02T00:00:00+0000"}],"nested":{"date":"2021-03-02T00:00:00.123Z"}}
```
The assertions for the direct and nested bean dates pass; the last one fails
with
`Collection element ignores the Views generator and loses milliseconds`. The
non-zero Gradle exit is
the expected reproduction of the defect, not a passing regression test. DTOs
deliberately have a
package: default-package classes trigger an unrelated template-name lookup
error. An explicit Servlet
API dependency is required on a standalone classpath.
### Environment Information
- Operating System: Linux
- JDK Version: 25 (GraalVM CE 25.2.4+7.1 and JBR 25.0.4+1.61067)
- Groovy: 5.1.x (8.0.x sources), 4.0.33 (7.2.4)
- Gradle: 9.8.1 (repository), 9.1.0 (standalone reproducer)
### Example Application
N/A — the two self-contained reproducers above (a Spock test and a
standalone Gradle project)
reproduce the issue without an application.
### Version
8.0.x (`8.0.1-SNAPSHOT` sources at `ba72113c99`). Also reproduces with the
published 7.2.4 artifacts.
---
### Source-level explanation (8.0.x)
`DefaultGrailsJsonViewHelper.processSimple` (lines 429-443) handles a bean
property whose generic
component type is not simple. At line 439 it constructs
`new groovy.json.StreamingJsonBuilder.StreamingJsonDelegate(out, true)`
**without a generator**.
Dates inside those elements are then written with the delegate's generator,
which falls back to
Groovy's default (`groovy.json.JsonOutput.DEFAULT_GENERATOR`, pattern
`yyyy-MM-dd'T'HH:mm:ssZ`;
`groovy-json` 4.0.33 and 5.1.3). The neighbouring branches of the same
method already use the view
generator: line 435 uses `getGenerator().toJson(...)` and the nested-bean
branch at lines 451-456
receives a delegate cloned from the surrounding generator-aware delegate,
which is why the direct and
nested bean dates are correct.
Changing only `grails.views.json.generator.dateFormat` cannot repair a
branch that never receives the
generator. Groovy is behaving according to its defaults; JSON Views should
propagate the options it
owns.
The same omission exists in the other delegate construction sites. All of
them were verified failing
with a test before the prepared fix:
| Site (8.0.x) | Reachable through
| Evidence |
|-----------------------------------------------|-----------------------------------------------------------------------------------|--------------|
| `DefaultGrailsJsonViewHelper:439` | `List<DTO>`, `DTO[]`, raw
`List` property of a POGO | verified red |
| `DefaultGrailsJsonViewHelper:107` | `g.inline(object)`
| verified red |
| `DefaultGrailsJsonViewHelper:204`, `:233` | child view that `inherits`
a parent and does `json g.render(object)` (inline) | verified red |
| `DefaultHalViewHelper:551`, `:558`, `:649` | `hal.embedded {
name(object) { ... } }` and `HalStreamingJsonDelegate` | verified
red |
In 7.2.4 the same branch lived at helper line 445, and the neighbouring
simple-element branch used
the static `JsonOutput.toJson(o)`. On 8.0.x that neighbouring branch already
uses
`getGenerator().toJson(...)` (line 435), so only the delegate propagation
part still matches the
7.2.4 report.
### Historical comparison
The inspected `views-json:1.2.10` sources also construct a delegate without
an explicit generator in
the equivalent collection branch (helper line 420), but they use Grails'
former builder, whose
`JsonOutput.java:62-77` configured the fallback as
`yyyy-MM-dd'T'HH:mm:ss'Z'` in GMT. Switching to
Groovy's builder changed the fallback to `+0000` and dropped milliseconds.
This is source
comparison, not a full Grails 3 runtime test; the expected precision in this
report comes from the
configured Grails 7/8 generator.
### Suggested Fix
Pass the configured Views generator (`getGenerator()`) when constructing
those delegates, and use it
for values that should honour generator options. Do not change Groovy's
global default or perform
string replacement on serialized JSON.
### Prepared fix
A fix with regression tests is prepared and will be submitted as a PR
referencing this issue
(proposed branch `fix/json-views-generator-propagation`, squash commit
`Fixes #<this issue>`):
* `grails-views-gson` only; no API changes, no new dependencies, no
documentation change needed
(the behaviour is already documented for `g.render`).
* Tests added: `List<DTO>`, `DTO[]`, raw `List`, `List<Date>` control, root
`List<DTO>` control,
configured `dateFormat`/`timeZone`, `g.inline`, template inheritance (POGO
and domain instance)
and HAL embedded callables. Each one fails before the fix and passes after
it.
* Validation: `./gradlew :grails-views-gson:test` -> 274 tests, 0 failures;
`./gradlew clean aggregateViolations :grails-test-report:check --continue`
-> Checkstyle, CodeNarc,
PMD, SpotBugs and REPOSITORY_CONVENTIONS reports with no violations. (The
repository-wide test
phase of that command also reports pre-existing locale-dependent failures
on a JVM started with
`-Duser.language=es`, in modules unrelated to this change.)
Please let me know if the scope should stay limited to the DTO collection
branch only; the prepared
PR currently fixes all the sites in the table above.
--
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]