JSON Views: a Date inside a DTO collection ignores the configured JsonGenerator and loses milliseconds
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 3/5
- Tempo stimato
- 1-2 giorni
- Idoneità per principianti
- 40/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- json
- Ambito
- backend
Direzione di ricerca
Start in grails-views-gson at DefaultGrailsJsonViewHelper.processSimple (around line 439), where a StreamingJsonDelegate is built without the view generator, then compare it with the neighbouring branches that call getGenerator(). The same omission is listed at DefaultGrailsJsonViewHelper lines 107, 204 and 233, and DefaultHalViewHelper lines 551, 558 and 649. Run the DateTimeHelperRenderingSpec test in that module with ./gradlew :grails-views-gson:test. Done when Dates in DTO lists, arrays and raw lists render with the configured generator format, matching the direct property.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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
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:
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):
{
"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
inheritsa parent and doesjson 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+0000instead of2025-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:
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
rootProject.name = 'json-view-generator-reproducer'
build.gradle
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
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:
{"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/16660-json-views-generator, squash commit Fixes #16660):
grails-views-gsononly; no API changes, no new dependencies, no documentation change needed
(the behaviour is already documented forg.render).- Tests added:
List<DTO>,DTO[], rawList,List<Date>control, rootList<DTO>control,
configureddateFormat/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.
- Lingua principale
- Groovy
- Stelle
- 2.9k
- Fork
- 978
- Merge medio
- 1g 2h
- PR unite (30g)
- 183
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di apache/grails-core
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
apache/grails-core#16635 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
apache/grails-core#16289 · 5 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
apache/grails-core#15764 ·
I maintainer di solito rispondono entro 1 giorno
-
Update documentation for Java 25Forse già presa @ruthst00 l’ha presa 11 giorni fa. Apertarelates-to: documentation weekly-action-item
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
apache/grails-core#15725 ·
I maintainer di solito rispondono entro 1 giorno
-
HHH000510 warning at startup for every lazy many-to-one association with Hibernate 7Forse già presa @matrei l’ha presa 1 giorno fa. Aperta
Difficoltà 3/5 Mezza giornata Idoneità per principianti 58/100
apache/grails-core#16659 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di apache/grails-core
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
polarsource/polar#15580 ·
I maintainer di solito rispondono entro 1 giorno
-
accepted bug via-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 69/100
pingdotgg/t3code#20931 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
bug/1-unconfirmed kind/bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 87/100
plone/plone.scale#167 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno