Repository navigation
8.0 (fix): Render dates and times in JSON the same way as Spring Boot, and bind them back as rendered - #16411
Conversation
Grails 8 now writes date and time values in JSON exactly as Spring Boot 4.1's default Jackson JsonMapper does, in both grails.converters.JSON and JSON views, including g.render. Fixes apache#16406: the JSON and XML DateMarshaller called toInstant(), which java.sql.Date and java.sql.Time do not support, and the JSON marshallers dropped a zero millisecond fraction and wrote every nanosecond of a Timestamp. - Date, java.sql.Date, Timestamp, Calendar and XMLGregorianCalendar render as a UTC instant with millisecond precision, formatted by the new JsonDateFormat, which follows Jackson's StdDateFormat - java.sql.Time renders as HH:mm:ss and LocalTime as ISO_LOCAL_TIME - OffsetTime, YearMonth, MonthDay, Duration, Period, ZoneId and javax.xml.datatype.Duration render as their ISO-8601 strings, Year and Month as numbers, TimeZone as its ID - Date, Calendar and ZonedDateTime map keys render as Jackson writes them - JSON views write dates in the configured timeZone with its offset unless grails.views.json.generator.dateFormat is set, and g.render writes these types through the view's generator - A Month binds from its number, so rendered JSON binds back instead of binding by ordinal The differences from Grails 7 are listed in the 8.0 upgrade guide.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16411 +/- ##
==================================================
+ Coverage 58.2100% 58.3488% +0.1388%
- Complexity 23964 24073 +109
==================================================
Files 2170 2174 +4
Lines 106815 106979 +164
Branches 19451 19479 +28
==================================================
+ Hits 62177 62421 +244
+ Misses 35825 35734 -91
- Partials 8813 8824 +11
🚀 New features to boost your workflow:
|
…nly where it reads the whole value
… Julian before 1582
…ding upgrade note
…a T, and not GraphQL
…dering-8.0.x # Conflicts: # grails-doc/src/en/guide/upgrading/upgrading80x.adoc
matrei
left a comment
There was a problem hiding this comment.
Reviewed at head 65a5a5b215 against 8.0.x (334007a8b5); merges cleanly.
What I ran, fresh (cleanTest + --no-build-cache, checked result XML timestamps):
| Module | Tests | Failures |
|---|---|---|
grails-web-common |
116 | 0 |
grails-converters |
195 | 0 |
grails-views-gson |
266 | 0 |
grails-views-core |
2 | 0 |
grails-databinding-core |
84 | 0 |
grails-databinding |
42 | 0 |
grails-web-databinding |
58 | 0 |
grails-test-suite-web |
443 | 0 |
grails-test-suite-persistence (MonthBindingSpec) |
4 | 0 |
codeStyle passes for grails-web-common, grails-converters, grails-views-gson, grails-databinding-core and grails-databinding.
Overall this looks good. The #16406 crash is fixed where it started, since the formatting now works from epoch millis. The Jackson-as-oracle parity specs are a strong way to hold the "same as Spring Boot" claim. I have one question about scope and a few smaller items.
Question
1. g.render now defers to any generator converter, not only the date/time ones.
DefaultJsonViewHelper.groovy:137, DefaultGrailsJsonViewHelper.groovy:491/494
hasConverter(type) asks the view's generator, which also holds the converters that applications register through ServiceLoader. As a result, when an application has a converter for one of its own types (a POGO or a domain class), that type now counts as a simple value in g.render:
- As a map value or collection element (
handleValue, line 336), it's written by the converter. Before, it went throughrenderTemplateOrDefault, so a_foo.gsontemplate for the type is now bypassed. - As a POGO property (
processSimple, line 412), it's written by the converter instead of being rendered property by property.
This is arguably more consistent, because json { foo value } already used the converter. It is still a behaviour change outside dates and times. If it's intended, could §76 say so? If not, hasConverter could be limited to the built-in converter types.
Minor
2. grails.converters.JSON date map keys ignore the configured Date marshaller. MapMarshaller.java:47, JSON.java:551
Keys always go through JsonDateFormat.formatKey, which is fixed UTC ISO. So with grails.converters.json.date = 'javascript', a DateMarshaller(format), or JSON.registerObjectMarshaller(Date) {...}, values follow the configuration and keys don't. It isn't a regression, since before the key was toString(). But the REST guide says keys render "the same way as those values", and JSON views do apply dateFormat to keys. Either qualify that sentence or note it in the upgrade guide.
3. Jsr310DateValueConverter.convert(Object, Closure) was replaced by convert(Object, DateTimeFormatter, Closure).
Jsr310ConvertersConfiguration.groovy:447
It's a public abstract class, so anything that subclasses it breaks. Being non-static inner makes that unlikely, but keeping the old signature as an overload that delegates would cost nothing.
4. monthValueConverter accepts non-integral numbers.
Jsr310ConvertersConfiguration.groovy:363
9.7 binds to SEPTEMBER through intValue(). Rejecting a Number with a fractional part would match how 13 is rejected.
5. grails-views-gson now uses org.grails.web.json.JsonDateFormat without declaring grails-web-common.
It resolves transitively today (through views-core/rest-transforms → web-core). An explicit implementation project(':grails-web-common') would follow the module's own convention of listing what it uses.
Nits
-
DateTimeValues.groovyexists twice with identical content (136 lines), ingrails-convertersandgrails-views-gsontests.grails-web-commonalready hastestFixtures, so it could live there once. -
The wording is hard to parse in a few places:
dataBinding.adoc:737: "a value written as ISO 8601 writes the type is bound as it names"upgrading80x.adoc:4282: "written as ISO 8601 writes it"- the Javadoc of
Jsr310DateValueConverter.convert
Something like "A value in the ISO 8601 form of the target type (as Grails renders it in JSON) is bound first, before any of those formats are tried" reads more easily.
-
The upgrade guide also fixes unrelated subsection levels and numbers (1.1, 17.1, 35.1→37.1, 51.x→52.x). The fixes are correct and fine to keep, but the PR description doesn't mention them.
-
The PR description refers to upgrade guide §75/§76. After the merge from
8.0.xthese are §76/§77.
Verified as correct
JsonDateFormatmatches Jackson'sStdDateFormat:- it works from epoch millis, so
java.sql.Date/Timenever calltoInstant(); - it reads fields from
GregorianCalendar, so it uses the Julian calendar before 1582; - BC years follow
_formatBCEYear(1 BC →+0000, 2 BC →-0001); - years after 9999 get
+; - the offset is written in minutes.
- it works from epoch millis, so
- The parity specs include
Month,Year, BC and 5-digit years, and pass against Jackson 3's defaultJsonMapper. - Marshaller ordering is correct:
SqlTimeMarshallercomes beforeDateMarshaller(only in default date mode;javascriptmode still writesnew Date(...)forTime), andMonthMarshallercomes beforeSimpleEnumMarshaller. The priority shift is documented. - For JSON views:
JsonViewGenerator.writeMapkeepsDefaultJsonGenerator's null-key, excluded-value and excluded-field handling;- it takes the custom path only when a date key is present;
Calendarvalues reach the overriddenwriteDatethroughDefaultJsonGenerator.writeObject;- an unset
dateFormatwith the defaultGMTgives the same output as the old default pattern, and the change for non-UTCtimeZone(-04→-04:00) is documented.
DateConversionHelper:- the ISO path applies only to values with an offset (
ZonedDateTime.parse), sodatetime-localstyle values still go to the configured formats; - the Julian-calendar read round-trips what
JsonDateFormatwrites before 1582; parseAllrejects a value that was only partly read;- the
SimpleDataBinderSpecchange fixes a test that only passed in a UTC JVM.
- the ISO path applies only to values with an offset (
- The specs that change
TimeZone.defaultrestore it incleanup. - The docs reference only public API. §8 and §24 are the right cross-references.
…r fixes - g.render treats a value as simple only when it is a date or time that the view's generator has a converter for. A converter an application registers for its own type no longer bypasses that type's template or property rendering (hasConverter becomes hasDateTimeConverter). - Keep Jsr310DateValueConverter.convert(value, callable), which reads only the configured formats, next to convert(value, iso, callable). - monthValueConverter rejects a number with a fractional part, such as 9.7, as it rejects 13. - grails-views-gson declares grails-web-common, for JsonDateFormat. - DateTimeValues lives once, in grails-web-common's test fixtures. - The guide says that date map keys keep the default form with a registered Date marshaller or the javascript date format, and rewords the ISO 8601 data binding sentences.
matrei
left a comment
There was a problem hiding this comment.
Re-reviewed at head a7d265dbef. It still merges cleanly into 8.0.x (334007a8b5).
Thanks, all five points and the nits are addressed.
What I ran, fresh (cleanTest + --no-build-cache, checked result XML timestamps):
| Module | Tests | Failures |
|---|---|---|
grails-web-common |
116 | 0 |
grails-converters |
195 | 0 |
grails-views-gson |
267 | 0 |
grails-views-core |
2 | 0 |
grails-databinding-core |
84 | 0 |
grails-databinding |
49 | 0 |
grails-web-databinding |
58 | 0 |
grails-test-suite-web |
443 | 0 |
grails-test-suite-persistence |
106 | 0 |
codeStyle passes for grails-web-common, grails-converters, grails-views-gson, grails-databinding-core and grails-databinding.
Checked
g.renderand application converters:hasDateTimeConvertercovers the type of every converter JSON views add:Month,Year,YearMonthandMonthDaythroughTemporalAccessor,DurationandPeriodthroughTemporalAmount,java.sql.TimethroughDate, plusZoneId,TimeZone,XMLGregorianCalendarand the XMLDuration.- An application converter for its own type now leaves
g.renderas it was before this PR. That includes the enumname()shortcut, which again ignores converters for enums other thanMonth. - I tested the new
ServiceLoadertest against the old behaviour. WithhasDateTimeConverterchanged back to accept any converter, the test fails, and it passes again with the PR's code.
- Date map keys: the REST guide and upgrade guide §76 now say that keys keep the default form when a
Datemarshaller orjavascriptis configured. convert(value, callable): it's back, and reads only the configured formats.FormatsOnlyLocalTimeConvertershows that a subclass using it still compiles and doesn't pick up the ISO 8601 path.monthValueConverter:- It uses
intValueExact(), so9.7,9.5fandNaNare rejected, and9.0still binds toSEPTEMBER. MonthBindingSpecchecks that9.7gives a binding error throughGrailsWebDataBinder.
- It uses
grails-web-common:grails-views-gsonnow declares it.
The nits are addressed too:
DateTimeValuesnow lives once, ingrails-web-commontest fixtures.- The ISO 8601 sentences in
dataBinding.adoc, upgrade guide §77 and the Javadoc read clearly now. - The PR description has the right section numbers and mentions the subsection fixes.
Nit
DateTimeHelperRenderingSpeccreates aURLClassLoaderover the@TempDirand never closes it. Wrapping it intry/finallywithclassLoader.close()(or acleanup:block) would avoid holding the directory open, which on Windows can make the@TempDircleanup fail.
LGTM.
The URLClassLoader over the @tempdir is closed in a cleanup block, so that the temporary directory can be deleted on Windows too.
|
Thanks! Closed the class loader in a |
This comment has been minimized.
This comment has been minimized.
jdaugherty
left a comment
There was a problem hiding this comment.
Thanks for the thorough work here, and for the very complete upgrade notes. I checked the branch against the 7.0.x sources for every touched type, and ran the six touched modules locally (769 tests, 0 failures). The #16406 fix is solid. My concern is about what else rides along with it, given where we are in the release cycle.
What this PR contains, split by kind of change
1. Regression fix: restores the Grails 7 output. The DateMarshaller/CalendarMarshaller changes, JsonDateFormat, the XML toInstant() guard, and the .SSS millisecond restoration. Date, Calendar, java.sql.Date and Timestamp render exactly as in 7.0.x again. This is what #16406 asks for, it carries no compatibility risk relative to 7, and it belongs in the next RC.
2. New behavior changes relative to Grails 7. These are Spring Boot / Jackson parity decisions rather than fixes. The upgrade guide documents all of them honestly, but each one changes output or binding that worked in 7.0.x:
java.time.Monthrenders as a number (9). 7.x rendered the enum object, 8.0 already documents"SEPTEMBER"(section 8), and this would be the third shape for one type across two releases. It also needs the newmonthValueConverterto round-trip.java.sql.Timerenders as"01:48:46"viatoString()in the JVM default zone. 7.x rendered it as a UTC instant in bothgrails.converters.JSONand JSON views. This is the one type that rendered fine in 7 and now changes both shape and time-zone semantics.Date/Calendarmap keys switch fromtoString()to ISO, in the converters, theJSONbuilder and JSON views.- JSON views with a configured
timeZonechange the offset form from-04to-04:00, anddateFormatno longer applies tojava.sql.Time. OffsetTimein JSON views drops zero seconds.- Date binding becomes strict: a configured format must consume the whole value (details below).
Jsr310DateValueConverter.convert(value, closure)now passes aDateTimeFormatterto the closure instead of theStringpattern.
The remaining new marshallers/converters (Year, YearMonth, Duration, ZoneId, TimeZone, ...) replace property-bag output or {} in 7, and application marshallers still take precedence, so I consider those low risk.
Binding: what a 7.x app sees
I replayed the 7.0.x DateConversionHelper loop and the new one over the default dateFormats, with the server in America/Denver:
| Input | Grails 7 | This PR |
|---|---|---|
2024-05-01T10:00:00Z |
16:00Z (read in server zone) | 10:00Z |
2024-05-01T10:00:00+02:00 |
16:00Z (offset ignored) | 08:00Z |
2024-05-01T10:00:00.5Z |
.005 |
.500 |
2024-05-01T10:00 (HTML datetime-local) |
midnight, time lost | binding error |
2024-05-01 10:00:00 |
midnight, time lost | binding error |
2024-05-01T10:00:00.123 (no zone) |
10:00 local, millis dropped | binding error |
2024-05-01 10:00:00Z |
midnight | binding error |
The first three rows are correctness fixes, but they silently shift persisted instants for any client that sends Z without milliseconds and was relying on the server-zone reading. The last four are the new whole-value rule. Three of those were silent data loss in 7, but the zone-less fraction case gave a usable answer in 7 and is now a hard error.
Proposal
We are on 8.0.0-RC1. Anything that lands after RC2 forces an RC3, so I would like to keep RC2 to the regression fix and take the parity changes to a wider discussion before deciding whether they are 8.0 or 8.1 material.
Concretely, I suggest splitting this PR:
- PR A (for RC2): group 1 above, plus the new marshallers/converters for the types that were unrenderable or rendered as property bags in 7. No
Month,java.sql.Time, map-key or binding changes. - PR B (needs discussion on the weekly / dev list):
Monthas a number plus its binding converter,java.sql.TimeastoString(), date map keys, the JSON viewstimeZone/OffsetTimechanges, and strict binding.
For PR B, two mitigations would remove most of the compatibility cost if we do decide to take it:
- Binding: try ISO first, then whole-value formats, then fall back to the 7.x prefix read as a last resort. That keeps every row above that 7 bound while still fixing the offset and fraction rows.
Month: keep"SEPTEMBER"for 8.0 and still addmonthValueConverter(it only claims numeric input), so clients sending either form bind.
Happy to help with the split if that is useful.
|
|
||
| public void marshalObject(Object object, JSON converter) throws ConverterException { | ||
| try { | ||
| converter.getWriter().value(((Month) object).getValue()); |
There was a problem hiding this comment.
This is the third shape for Month across two releases: the enum object in 7.x, "SEPTEMBER" per section 8 of the 8.0 guide, and now 9. It is also the only enum that behaves differently from every other enum, and it needs monthValueConverter just to round-trip.
I would keep "SEPTEMBER" for 8.0 and leave the numeric form for the wider parity discussion. monthValueConverter can stay either way, since it only claims numeric input and makes both forms bind.
There was a problem hiding this comment.
Kept as a number.
- On 9.0, Write grails.converters.JSON through Spring Boot's JsonMapper #16414 renders
Monththrough Boot'sJsonMapperas9anyway, so"SEPTEMBER"in 8.0 would mean a second change in 9 forgrails.converters.JSONusers. - JSON views users see a single change, since 7 already wrote
"SEPTEMBER". monthValueConverterbinds both forms, and §76.4 shows the opt-out.- The registration is in
ConvertersConfigurationInitializernow (f54c009).
There was a problem hiding this comment.
Two points the reply doesn't cover:
- Apps that followed the 7.0.2 deprecation notice already render
"SEPTEMBER". 7.0.x readsgrails.converters.json.enum.format: simple, the setting §8 now tells people they can remove. Those apps renderMonthas"SEPTEMBER"ingrails.converters.JSONtoday, as JSON views do. For them, and for every JSON views user,9is a new change. It also breaks, for this one enum, the §8 contract we announced ("enums are serialized as simple string values"). The XML converter still writesSEPTEMBER, so the same object would render as9in JSON andSEPTEMBERin XML. - Write grails.converters.JSON through Spring Boot's JsonMapper #16414 is an open PR against 9.0.x, not a decided direction. Whether 9.0 renders
grails.converters.JSONthrough Boot'sJsonMapperis a 9.0 question, and a PR still under review shouldn't settle 8.0 behavior.
Please keep Month rendering as "SEPTEMBER" in 8.0:
- Drop the
Monthregistration fromConvertersConfigurationInitializerandJsonViewTemplateEngine. - Keep
monthValueConverter, which binds9,"9"and"SEPTEMBER"and is a good change either way.
I tried this locally. The only failures are the assertions that pin Month as a number, in JsonDateTimeRenderingSpec, DateTimeRenderingSpec and DateTimeHelperRenderingSpec. MonthBindingSpec and DateTimeRoundTripBindingSpec still pass.
On the docs side, the Month rows in §76.2, the Month snippet in §76.4, and the Month entries in defaultRenderers.adoc and jsonConfiguration.adoc come out. The §76.4 enum note would then cover Month too: Grails renders every enum by name(), where Jackson 3 writes Month as its number.
There was a problem hiding this comment.
Done in f8ef378. Month renders as "SEPTEMBER" in both renderers, and monthValueConverter stays.
- The
Monthrows and snippets are out of the guides, and the §76.4 enum note coversMonth. - The guides show a marshaller and a JSON views converter that render its number, as Spring Boot does, and tests cover both.
|
|
||
| public void marshalObject(Object object, JSON converter) throws ConverterException { | ||
| try { | ||
| converter.getWriter().value(object.toString()); |
There was a problem hiding this comment.
In 7.0.x a java.sql.Time went through DateMarshaller and rendered as a UTC instant ("1970-01-01T01:48:46.000Z"), and JSON views did the same through writeDate. This changes it to a local time-of-day in the JVM default zone, so both the shape and the zone semantics move for a type that rendered fine in 7.
Jackson does write toString() here, so the parity claim is right, but this is not part of the #16406 regression. I would leave Time on the Date path for RC2 and decide the parity question separately.
There was a problem hiding this comment.
Kept.
- The 7.x form doesn't bind back:
"1970-01-01T08:48:46.000Z"is atypeMismatchfor ajava.sql.Timeproperty. "01:48:46"binds back to the sameTime(bd89825).- §76.4 shows a marshaller that restores the 7.x string, and a test covers it.
There was a problem hiding this comment.
Confirmed: binding "1970-01-01T08:48:46.000Z" to a java.sql.Time property is a typeMismatch (I ran it), so the 7.x form never round-tripped. I'm fine keeping toString().
One precision point for §76.2 (line 4210): "binds back to the same java.sql.Time" holds only for a Time on 1970-01-01 with zero milliseconds, like the Time.valueOf('01:48:46') in the test. new Time(1759909726407L) (2025-10-08T07:48:46.407Z) binds back as that time of day on 1970-01-01, without the milliseconds, as with Jackson. "binds back to the same time of day, to the second" would be accurate.
There was a problem hiding this comment.
Fixed in 76d01c9: "which binds back to the same time of day, to the second, as with Spring Boot".
| try { | ||
| formatter.lenient = dateParsingLenient | ||
| dateValue = formatter.parse((String) value) | ||
| dateValue = parseAll(formatter, (String) value) |
There was a problem hiding this comment.
This is the part of the binding change I think needs discussion before it goes into an RC. With the default dateFormats, which still include yyyy-MM-dd, the whole-value rule turns these 7.x results into binding errors:
| Input | 7.0.x | now |
|---|---|---|
2024-05-01T10:00 (HTML datetime-local) |
midnight | error |
2024-05-01 10:00:00 |
midnight | error |
2024-05-01T10:00:00.123 (no zone) |
10:00 local, millis dropped | error |
The first two were silent data loss in 7, so an error is arguably better, but the third gave a usable value. A cheap way to keep 7.x compatibility while still fixing the offset and fraction cases: ISO first, then whole-value formats, then fall back to the 7.x prefix read (formatter.parse(value)) only when nothing else matched.
There was a problem hiding this comment.
Done as you suggested, in 778d072: ISO, then a format that reads the whole value, then the 7.x read.
- Rows 4–7 bind as in 7.x again.
- The fallback also covers
…T10:00:00.123+0200, a custom format matching only the start, andTimestamp#toString()with nanos, which the whole-value rule had also broken.
| if (dateValue == null) { | ||
| try { | ||
| dateValue = (T) callable.call(format) | ||
| dateValue = (T) callable.call(DateTimeFormatter.ofPattern(format)) |
There was a problem hiding this comment.
The closure passed to convert(value, callable) used to receive the String pattern and now receives a DateTimeFormatter. The subclass test was rewritten for that, but a 7.x subclass written as convert(value) { String format -> X.parse(value, DateTimeFormatter.ofPattern(format)) } now fails at runtime with a MissingMethodException.
This is an org.grails internal, so it may be acceptable, but if we keep it the upgrade guide should say so. Alternatively keep passing the pattern string and build the formatter inside the closure as before.
There was a problem hiding this comment.
Fixed in 77fa0e0.
convert(value, callable)passes theStringpattern again, so a 7.x subclass works unchanged.FormatsOnlyLocalTimeConverterin the spec is written the 7.x way now ({ String format -> … }).
| render(file: new File(absolutePath), inline: true) | ||
| ---- | ||
|
|
||
| ==== 76. JSON Rendering of Dates and Times Matches Spring Boot |
There was a problem hiding this comment.
The table here is accurate and I appreciate how complete it is. One suggestion once the split is settled: separate what 8.0.0 restores from 7 (the .SSS millisecond form and the java.sql types) from what it changes relative to 7, so a reader upgrading from 7.x can tell at a glance which rows require action.
There was a problem hiding this comment.
Done in ecee9bf: §76.1 is what 8.0.0 restores from 7, §76.2 what renders differently than in 7, §76.3 what 7 didn't render as a value, and §76.4 how to keep the 7 rendering.
sbglasius
left a comment
There was a problem hiding this comment.
Automated code review: a few hunk-visible correctness inconsistencies spotted while reviewing the date/time rendering changes. Overall the change is thoroughly tested; these are edge cases worth a second look.
| MappingFactory mappingFactory = jsonView.mappingContext?.mappingFactory | ||
| if (mappingFactory != null) { | ||
| return mappingFactory.isSimpleType(propertyType) || (value instanceof Enum) || (value instanceof Map) | ||
| return mappingFactory.isSimpleType(propertyType) || (value instanceof Enum) || (value instanceof Map) || hasDateTimeConverter(propertyType) |
There was a problem hiding this comment.
This override of isSimpleType(Class propertyType, value) checks hasDateTimeConverter(propertyType) (the declared type), whereas the base DefaultJsonViewHelper.isSimpleType and the sibling check in processSimpleProperty (!hasDateTimeConverter(value.getClass())) both use the runtime value's class.
For a GORM property declared as a supertype/interface (e.g. Object) that at runtime holds a Month/Year/Duration value, this would evaluate hasDateTimeConverter against the non-date declared type and return false, so the property would not be classified as simple and would be traversed as a complex object instead of going through the registered date/time converter. Should this use value?.getClass() ?: propertyType like the other call sites?
There was a problem hiding this comment.
That line is in isSimpleValue(Object value), where propertyType is value.getClass(). So it checks the runtime type, as the other call sites do.
|
|
||
| private static boolean isDateTimeType(Class<?> type) { | ||
| TemporalAccessor.isAssignableFrom(type) || TemporalAmount.isAssignableFrom(type) || | ||
| ZoneId.isAssignableFrom(type) || TimeZone.isAssignableFrom(type) || Date.isAssignableFrom(type) || |
There was a problem hiding this comment.
isDateTimeType checks Date.isAssignableFrom(type) but doesn't include Calendar, even though Calendar is treated symmetrically with Date everywhere else in this file (formatMapKey, hasDateKey/isDateKey, formatDate). As written, hasDateTimeConverter(Calendar) always returns false, even though Calendar values are in fact written through the date/time formatting path. Was the omission intentional?
There was a problem hiding this comment.
Intentional.
- No converter handles
Calendar, sohasDateTimeConverter(Calendar)would be false even ifisDateTimeTypeincluded it. Calendaris already aMappingFactorysimple type, and the generator writes it with the date format.
| Map.Entry element = (Map.Entry) o; | ||
| writer.key(String.valueOf(element.getKey())); //.value(element.getValue()); | ||
| Object elementKey = element.getKey(); | ||
| writer.key(elementKey == null ? "null" : JsonDateFormat.formatKey(elementKey)); |
There was a problem hiding this comment.
For a null key this writes the literal string "null", but MapMarshaller.java (touched in this same PR, also now calling JsonDateFormat.formatKey) silently skips map entries whose key is null instead. That means the same map with a null key serializes differently depending on whether it goes through the JSON builder DSL (json.build { keyed(...) }) or through ([...] as JSON)/MapMarshaller. Not introduced by this line specifically (the null handling predates this diff), but worth reconciling now that both paths route through JsonDateFormat.
There was a problem hiding this comment.
Both paths behave as in 7.x: the builder wrote String.valueOf(key), which gives "null", and MapMarshaller skipped the entry. This PR only changes how date keys are written.
#16411 added a dedicated ObjectMarshaller/JsonGenerator.Converter class per java.time/java.sql/javax.xml value type, following the pre-existing one-class-per-type convention in these packages. Each of those new classes was 100% boilerplate: an instanceof/isAssignableFrom check and one line computing the JSON value. Replace the 13 single-type JSON marshaller classes and 10 single-type JSON-views converter classes the PR added with two small generic implementations, SimpleTypeMarshaller<T> and SimpleTypeJsonConverter, each parameterized by the type and a value-extracting function. Registration order (and therefore marshaller/converter priority) is unchanged. Pre-existing marshallers/converters that predate #16411 are left as-is. Verified behavior-preserving: grails-converters (195 tests) and grails-views-gson (266 tests) pass unchanged, including the parity specs added by #16411 that compare output against a real Jackson JsonMapper. checkstyle and CodeNarc are clean for both modules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extend the previous commit's generic marshaller/converter to the 5 JSON marshallers and 8 JSON-views converters that predate #16411 (Instant, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, OffsetTime, Period, ZonedDateTime), removing the same one-line-of-logic boilerplate from the pre-existing code, not just the code #16411 added. These files carried individual @author tags. The repo's established convention is not to carry personal attribution in source (recent examples: e6fa4eb, 59d78b2, b7ab39b, 87fb269, and 17debfa removing a real author's name/email from a plugin descriptor "as this information is not necessary for core plugins") — git history is the attribution record, so dropping them here follows existing practice rather than breaking from it. Verified behavior-preserving the same way as the previous commit: grails-converters (195 tests) and grails-views-gson (266 tests) pass unchanged, checkstyle/CodeNarc clean on both modules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
jdaugherty's review on #16411 proposes splitting the PR: a PR A for RC2 (the #16406 regression fix plus new marshallers for types that were unrenderable in 7.x, called out as low risk) and a PR B, needing wider discussion, for the actual Spring Boot/Jackson parity behavior changes (Month as a number, java.sql.Time as toString() with a different time zone, and the JSON-views OffsetTime format change, among others not touched here). Rewrite the dedup work (previously 3 commits, now squashed into this one and its test-coverage follow-up) to leave every file jdaugherty flagged as PR-B material completely untouched: - MonthMarshaller.java / MonthJsonConverter.groovy (Month as a number) - SqlTimeMarshaller.java / SqlTimeJsonConverter.groovy (java.sql.Time as toString()) - OffsetTimeJsonConverter.groovy (the JSON-views format change from ISO_OFFSET_TIME to toString(), predates this collapse but is the same compatibility-risk item) Every other single-type marshaller/converter this PR added or that predates it — Year, YearMonth, MonthDay, Duration, Period, ZoneId, TimeZone, XMLGregorianCalendar, the javax.xml Duration, LocalTime, OffsetTime (grails.converters.JSON only, a new addition with no pre-8.0 behavior to diverge from), Instant, LocalDate, LocalDateTime, OffsetDateTime and ZonedDateTime — collapses into SimpleTypeMarshaller (grails-converters) / SimpleTypeJsonConverter (grails-views-gson) as before. Zero diff on the five untouched files, confirmed with git diff --stat. This makes the branch apply the same way regardless of whether it lands on the full PR or on a future split-off PR A: it never creates or resolves a conflict against the parts that split would carve out. Verified: grails-converters (199 tests) and grails-views-gson (270 tests) pass, checkstyle/CodeNarc clean on both modules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
There's a branch,
It deliberately leaves Given how much of this PR's diff is that repeated boilerplate, I'd highly suggest merging this consolidation in — either into this PR or as a fast-follow — since it meaningfully shrinks the surface reviewers have to read through. |
…at that reads its start A format that reads all of a value is still tried first, so it wins over an earlier format that reads only the start of it. Values such as 2024-05-01T10:00, 2024-05-01T10:00:00.123 and 2024-05-01T10:00:00.123+0200 bind as they did in Grails 7 instead of failing.
…ails 7 did A Jsr310DateValueConverter subclass written for Grails 7 receives the String pattern again. The built-in converters use convert(value, iso, callable), which tries the ISO 8601 form first and passes a DateTimeFormatter.
JSON views keep their default dateFormat, yyyy-MM-dd'T'HH:mm:ss.SSSX, so a configured timeZone writes the offset as before, and OffsetTime keeps its ISO_OFFSET_TIME form, which grails.converters.JSON now uses too. In the default GMT, dates render as Spring Boot renders them.
…aller and converter The 13 JSON marshallers and 10 JSON view converters added for Month, java.sql.Time, LocalTime, OffsetTime, Year, YearMonth, MonthDay, Duration, Period, ZoneId, TimeZone, XMLGregorianCalendar and the XML Duration become registrations of an internal SimpleTypeMarshaller and SimpleTypeJsonConverter, in the same order, so they render and take precedence as before. The marshallers and converters that Grails 7 shipped are unchanged. Adapted from Walter Duque de Estrada's refactor/16411-marshaller-dedup branch.
…at changes from Grails 7 The note lists what 8.0.0-RC1 changed and 8.0.0 restores, the values that render differently than in Grails 7, and the types Grails 7 did not render as values, with a marshaller for java.sql.Time and Month that keeps their Grails 7 rendering, tested as the guide shows it.
|
Kept as one PR. Your mitigations are in, and three changes stay, with the reasons below.
Kept, since without them a Grails app renders these differently than the same Spring Boot app:
Each of the three is one registration plus its docs. If the weekly prefers the 7.x form for any of them, it's one commit on this PR. |
|
@borinquenkid Thanks. f54c009 collapses the marshallers and converters this PR adds into
|
addressed and pending re-review
jdaugherty
left a comment
There was a problem hiding this comment.
Re-reviewed at 3705691ce2, which merges cleanly into 8.0.x. Thanks for taking the mitigations on. I re-ran the touched modules fresh (grails-web-common, grails-converters, grails-views-gson, grails-databinding-core, grails-databinding, grails-web-databinding, grails-test-suite-web, MonthBindingSpec): 1,225 tests, 0 failures, and codeStyle is clean.
Checked against the 7.0.x sources:
- Binding (778d072): ISO first, then a format that reads the whole value, then the 7.x read of the start of the value. Every row of my earlier table that bound in 7 binds the same way again, and the offset and fraction rows keep the fix. The Denver-zone rows in
DateConversionHelperSpeccover exactly those cases. convert(value, callable)(77fa0e0) passes theStringpattern again, and the ISO overload is additive, so a 7.x subclass works unchanged.- JSON views (71359da): the
dateFormatdefault, the unconditionaloptions.dateFormat(...)andOffsetTimeJsonConvertermatch 7.0.x again, so a configuredtimeZoneandOffsetTimerender as in 7. - Generic marshaller and converter (f54c009): none of the 23 removed classes exists in 7.0.x or 8.0.0-RC1, so nothing public goes away, and the registration order is unchanged.
- Upgrade guide (ecee9bf): the 76.1 to 76.4 split reads well.
Of the three changes you kept, the java.sql.Time change and the map keys hold up for me (details on the Time thread). Two things I'd like changed before this merges, each on its own thread:
Monthshould keep rendering as"SEPTEMBER"in 8.0.- The JSON views default date format writes the wrong instant for a configured
timeZonewhose offset is not a whole number of hours.
|
|
||
| public void marshalObject(Object object, JSON converter) throws ConverterException { | ||
| try { | ||
| converter.getWriter().value(((Month) object).getValue()); |
There was a problem hiding this comment.
Two points the reply doesn't cover:
- Apps that followed the 7.0.2 deprecation notice already render
"SEPTEMBER". 7.0.x readsgrails.converters.json.enum.format: simple, the setting §8 now tells people they can remove. Those apps renderMonthas"SEPTEMBER"ingrails.converters.JSONtoday, as JSON views do. For them, and for every JSON views user,9is a new change. It also breaks, for this one enum, the §8 contract we announced ("enums are serialized as simple string values"). The XML converter still writesSEPTEMBER, so the same object would render as9in JSON andSEPTEMBERin XML. - Write grails.converters.JSON through Spring Boot's JsonMapper #16414 is an open PR against 9.0.x, not a decided direction. Whether 9.0 renders
grails.converters.JSONthrough Boot'sJsonMapperis a 9.0 question, and a PR still under review shouldn't settle 8.0 behavior.
Please keep Month rendering as "SEPTEMBER" in 8.0:
- Drop the
Monthregistration fromConvertersConfigurationInitializerandJsonViewTemplateEngine. - Keep
monthValueConverter, which binds9,"9"and"SEPTEMBER"and is a good change either way.
I tried this locally. The only failures are the assertions that pin Month as a number, in JsonDateTimeRenderingSpec, DateTimeRenderingSpec and DateTimeHelperRenderingSpec. MonthBindingSpec and DateTimeRoundTripBindingSpec still pass.
On the docs side, the Month rows in §76.2, the Month snippet in §76.4, and the Month entries in defaultRenderers.adoc and jsonConfiguration.adoc come out. The §76.4 enum note would then cover Month too: Grails renders every enum by name(), where Jackson 3 writes Month as its number.
|
|
||
| public void marshalObject(Object object, JSON converter) throws ConverterException { | ||
| try { | ||
| converter.getWriter().value(object.toString()); |
There was a problem hiding this comment.
Confirmed: binding "1970-01-01T08:48:46.000Z" to a java.sql.Time property is a typeMismatch (I ran it), so the 7.x form never round-tripped. I'm fine keeping toString().
One precision point for §76.2 (line 4210): "binds back to the same java.sql.Time" holds only for a Time on 1970-01-01 with zero milliseconds, like the Time.valueOf('01:48:46') in the test. new Time(1759909726407L) (2025-10-08T07:48:46.407Z) binds back as that time of day on 1970-01-01, without the milliseconds, as with Jackson. "binds back to the same time of day, to the second" would be accurate.
| * default {@link #timeZone} it writes a UTC instant with millisecond precision, such as | ||
| * {@code 2024-06-15T14:30:45.123Z}, as Spring Boot does. | ||
| */ | ||
| String dateFormat = /yyyy-MM-dd'T'HH:mm:ss.SSSX/ |
There was a problem hiding this comment.
Restoring the 7.x default, which I asked for, also restores a 7.x bug. With a single X, SimpleDateFormat writes only the hours of the offset ("any fraction of an hour is ignored"). So with a configured timeZone whose offset is not a whole number of hours, the rendered instant is wrong. I rendered 2025-10-08T07:48:46.407Z through a views generator in each zone and parsed the result back:
timeZone |
written with X (this default) |
parses back as | written with XXX |
|---|---|---|---|
Asia/Kolkata |
2025-10-08T13:18:46.407+05 |
08:18:46.407Z, 30 minutes late |
2025-10-08T13:18:46.407+05:30 |
Asia/Kathmandu |
2025-10-08T13:33:46.407+05 |
08:33:46.407Z, 45 minutes late |
2025-10-08T13:33:46.407+05:45 |
America/New_York |
2025-10-08T03:48:46.407-04 |
correct | 2025-10-08T03:48:46.407-04:00 |
GMT (default) |
2025-10-08T07:48:46.407Z |
correct | 2025-10-08T07:48:46.407Z |
Please default to yyyy-MM-dd'T'HH:mm:ss.SSSXXX. The default GMT output doesn't change, every zone parses back to the right instant, and the XXX column matches JsonMapper with the same default time zone character for character. Whole-hour zones change from -04 to -04:00, which needs a §76.2 row.
With that change locally, the only failing test is the America/New_York row of DateTimeHelperRenderingSpec ("a configured timeZone writes … as Grails 7 did"). Please also add a half-hour row there, such as Asia/Kolkata, since only whole-hour zones are covered. The -04 form also appears in:
- the Time Zone and Date Format sections of
jsonConfiguration.adoc; - the
defaultValueinadditional-spring-configuration-metadata.json; - the last §76.4 note.
There was a problem hiding this comment.
Done in 09bf0ce. The default is yyyy-MM-dd'T'HH:mm:ss.SSSXXX.
- The
timeZonetest compares withJsonMapperforGMT,America/New_York,Asia/KolkataandAsia/Kathmandu. - §76.2 has a row for the offset change, and
jsonConfiguration.adoc, the metadatadefaultValueand the last §76.4 note are updated.
…as well Month keeps rendering as "SEPTEMBER" in grails.converters.JSON and JSON views, as section 8 of the upgrade guide describes for every enum and as the XML converter writes it. monthValueConverter still binds 9 and "9" to SEPTEMBER. The guide shows a marshaller and a JSON views converter that render a Month as its number, as Spring Boot does, and tests cover both, including a domain property that g.render writes.
…s Spring Boot does The default grails.views.json.generator.dateFormat becomes yyyy-MM-dd'T'HH:mm:ss.SSSXXX. The single X of Grails 7 wrote only the hours of the offset, so a configured timeZone such as Asia/Kolkata named an instant 30 minutes off. The default GMT output does not change, and a configured timeZone now renders as Spring Boot renders it with spring.jackson.time-zone.
|
Both changed:
|
jdaugherty
left a comment
There was a problem hiding this comment.
Re-reviewed at 76d01c9dd1. Thanks, both asks are in:
Monthrenders as"SEPTEMBER"with both renderers again,monthValueConverterstill binds9, and the guides show how to get Spring Boot's number. The newg.rendertest pins the converter path: with the!hasDateTimeConverter(...)guard taken off the enum branch ofDefaultGrailsJsonViewHelper, it fails with{"month":"SEPTEMBER"}.- JSON views default to
yyyy-MM-dd'T'HH:mm:ss.SSSXXX, and thetimeZonetest now compares withJsonMapperfor GMT, New York, Kolkata and Kathmandu.
I re-ran the touched modules fresh (grails-web-common, grails-converters, grails-views-gson, grails-databinding-core, grails-databinding, grails-web-databinding, grails-test-suite-web, MonthBindingSpec): 1,223 tests, 0 failures, and codeStyle is clean for the three modules this round changed.
Approving, on the condition that these two are fixed before this merges:
- The merge conflict with
8.0.xinupgrading80x.adoc. #16275 added its own section 76, "A List of Objects in Configuration Binds to Its Declared Type", after the same section 75, so these two sections become 77 and 78, with subsections 77.1 to 77.4. Nothing else in the guides refers to them by number. - The three comments that still say Grails renders a
Monthas its number:- the
monthValueConverterJavadoc inJsr310ConvertersConfiguration(suggestion inline) - the class Javadoc of
MonthBindingSpec - the feature name
monthValueConverter binds a month number, as grails.converters.JSON and JSON views render a MonthinJsr310ConvertersConfigurationSpec
- the
| * Binds a {@link Month} from its number, 1 for January through 12 for December, which is how | ||
| * {@code grails.converters.JSON}, JSON views and Spring Boot render a Month. Without it a number would bind | ||
| * through Spring's conversion service by ordinal, one month late. A month name still binds as any enum does. |
There was a problem hiding this comment.
Since f8ef378 this no longer holds: grails.converters.JSON and JSON views render a Month as "SEPTEMBER", and only Spring Boot writes the number. Something like:
| * Binds a {@link Month} from its number, 1 for January through 12 for December, which is how | |
| * {@code grails.converters.JSON}, JSON views and Spring Boot render a Month. Without it a number would bind | |
| * through Spring's conversion service by ordinal, one month late. A month name still binds as any enum does. | |
| * Binds a {@link Month} from its number, 1 for January through 12 for December, which is how Spring Boot | |
| * renders a Month. Without it a number would bind through Spring's conversion service by ordinal, one month | |
| * late. The name that {@code grails.converters.JSON} and JSON views render binds as any enum does. |
Two tests make the same claim:
- the class Javadoc of
MonthBindingSpec(line 30) - the feature name
monthValueConverter binds a month number, as grails.converters.JSON and JSON views render a MonthinJsr310ConvertersConfigurationSpec(line 299)
There was a problem hiding this comment.
Applied in ede7ecf, with the MonthBindingSpec class Javadoc and the Jsr310ConvertersConfigurationSpec feature name.
…dering-8.0.x # Conflicts: # grails-doc/src/en/guide/upgrading/upgrading80x.adoc
…e a Month binds from one
|
Both done:
|
Apache Grails tagged 8.0.0-RC2 on 2026-09-29. RC1 has since been promoted to Maven Central, but RC2 is again in the ASF staging group only while its release vote runs, so the TEMP staging-repository entries in build.gradle stay and their comments are retargeted at RC2. RC2's BOM keeps Spring Boot 4.1.1 and Groovy 5.1.3; the cloud.wondrify asset-pipeline plugin follows the BOM to 5.2.0-RC3 (on Maven Central). RC2 itself is built on Gradle 9.8.0, the current release, so the wrapper moves from 9.7.1 to 9.8.0 to stay on the combination Grails tests. This is alignment rather than a requirement: RC2 still assembles on 9.7.1. .gitlab-ci.yml moves to bbb-build:grails-8--2026-10-02-140644, rebuilt today with the Grails 8.0.0-RC2 CLI. It also picks up the toolchain the current v4.0.x-release image has (Go 1.27.0, Node 22.23.2), which the previous grails-8 image predated. Behavior change that comes with RC2 (apache/grails-core#15967): Grails now sends browser-hardening headers on every response by default, so every bbb-web response gains X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Referrer-Policy: strict-origin-when-cross-origin X-XSS-Protection: 0 The defaults are left in place. Joining through a cross-origin iframe keeps working: the join call answers with a redirect, and the framed document is the client served by nginx, which carries no such header. What changes is that a document rendered by bbb-web itself (for example the XML of /bigbluebutton/api) is no longer displayed inside a cross-origin frame. A deployment that needs that can opt out per header in /etc/bigbluebutton/bbb-web.properties, e.g. grails.security.headers.frame-options.enabled=false. Checked, nothing to do: the RC2 JSON date/time rendering change (apache/grails-core#16411) concerns grails.converters.JSON and JSON views, while bbb-web builds its JSON with groovy.json.JsonBuilder and ships no gson views; the link-resolution change (apache/grails-core#16272) has nothing to act on, bbb-web's only generated link is one redirect(action:). Verified together with the Tomcat pin in the next commit.
Description
Fixes #16406. Grails 8 renders date and time values in JSON the way Spring Boot 4.1 does (its default Jackson 3
JsonMapper), in bothgrails.converters.JSON(render ... as JSON,respond) and JSON views. It also keeps what Grails 7 applications rely on: enums,Monthincluded, rendered by name,OffsetTime, and date binding.The regression in #16406
Two 8.0.x-only commits from #15432 changed the default JSON
DateMarshaller/CalendarMarshaller(and the XMLDateMarshaller):2fcf83213areplacedFastDateFormatwithdate.toInstant().java.sql.Date#toInstant()andjava.sql.Time#toInstant()always throwUnsupportedOperationException, so rendering either one failed (HTTP 500 from a controller).d3c82799f0switched toDateTimeFormatter.ISO_INSTANT, which drops a zero fraction (...T03:00:00Zinstead of...T03:00:00.000Z) and writes every nanosecond of aTimestamp.Grails 7.x never had either problem. Its
.SSS'Z'output is what the 7.0 upgrade guide documents, and what Spring Boot renders. This PR restores it.Spring Boot parity
With this change a Grails app and a Spring Boot app return the same JSON for the same values:
Date,java.sql.Date,Timestamp,Calendar,XMLGregorianCalendar"2025-10-08T07:48:46.407Z": UTC, always millisecond precision,Timestampnanos truncatedjava.sql.Time"01:48:46"(Time#toString()), which binds back to the same time of day, to the secondInstant,LocalDate,LocalDateTime,OffsetDateTime,ZonedDateTimeLocalTime"01:48:46.407254"(ISO_LOCAL_TIME)OffsetTime"01:48:46.407254-06:00"(ISO_OFFSET_TIME, as JSON views already rendered it)YearMonth,MonthDay,Duration,Period,javax.xml.datatype.DurationtoString(), e.g."2026-09","--09-25","PT1H30M"Year2026ZoneId,ZoneOffset,TimeZone"America/Sao_Paulo","-03:00"Date/Calendar/java.sqlmap keysZonedDateTimemap keysISO_OFFSET_DATE_TIMEDate/Calendarformatting ingrails.converters.JSONlives in a neworg.grails.web.json.JsonDateFormat(grails-web-common). It follows Jackson'sStdDateFormat:java.sqltypes are safe;java.util.Datedoes;+12345,-0043).grails.converters.JSON:SimpleTypeMarshallerinConvertersConfigurationInitializer.java.sql.Timeis ahead ofDateMarshaller, andMonthis ahead ofSimpleEnumMarshaller.MapMarshallerand theJSONbuilder write date keys throughJsonDateFormat.formatKey. They keep that default form even where a registeredDatemarshaller or thejavascriptdate format changes how the values render (documented).JSON.registerObjectMarshaller(...)still take precedence. The marshallers Grails 7 shipped are unchanged.JSON views: the added types are registrations of one internal
SimpleTypeJsonConverter. A smallDefaultJsonGeneratorsubclass,JsonViewGenerator, writes date map keys with the configureddateFormat,timeZoneandlocale, as the values are written. Keys that format to the same text are all kept, as Jackson keeps them.grails.views.json.generator.dateFormatbecomesyyyy-MM-dd'T'HH:mm:ss.SSSXXX. It writes the same UTC instant as Spring Boot in the defaultGMT, and a configuredtimeZonewith its offset in hours and minutes, asspring.jackson.time-zonedoes. The singleXof Grails 7 wrote only the hours, so a zone such asAsia/Kolkatanamed an instant 30 minutes off.spring.jackson.date-format, the date format does not apply tojava.sql.Timevalues.g.render(DefaultGrailsJsonViewHelper) now goes through the generator for these types:Year/Duration/ZoneId… rendered as{}. A converter an application registers for any other type does not change howg.renderrenders it: its template, or its properties, still apply;name()shortcut no longer bypasses a converter, so an application's converter forMonthapplies to a domain property too;ServiceLoaderstill take precedence. The converters Grails 7 shipped are unchanged.Data binding: a
Monthbinds from its number, as Spring Boot writes and reads it, as well as from its name.{"month":9}bound through Spring'sIntegerToEnumConverterFactoryby ordinal, silently givingOCTOBER.monthValueConverterinJsr310ConvertersConfigurationbinds9/"9"toSEPTEMBER, matching Jackson's reading. Month names still bind as before.13or9.7, is a binding error.Dates and times bind back as they render, too:
grails.databinding.dateFormatsare tried.Dateis read in the calendar it is written in, Julian before 1582, as it renders.java.timetypes try their ISO 8601 form first, through the newJsr310DateValueConverter.convert(value, iso, callable).convert(value, callable)still passes theStringpattern to its closure, as in Grails 7.Before, on a server outside UTC:
2024-05-01T10:00:00Zwas bound in the server's zone.+02:00offset without milliseconds was dropped..5was read as 5 milliseconds.OffsetDateTimeorZonedDateTime, and aLocalDateTimeorLocalTimewith a fraction, could not be bound from the form Grails renders it in.XML: only the #16406 crash is fixed.
java.sql.Date/Timeno longer calltoInstant(). The XML date format (ISO offset date-time since #15432) is unchanged; its difference from 7.x is now documented in the upgrade guide.Not changed
Monthamong them, still render byname()(upgrade guide §8), as the XML converter writes them. Jackson 3 writes an enum'stoString(), and aMonthas its number.Month("SEPTEMBER"here,9in Spring Boot) and for enums that overridetoString(), e.g.ChronoUnit.SECONDSrenders as"SECONDS"here and"Seconds"in Spring Boot.Monthas its number.OffsetTimekeeps its seconds and writes a fraction with only the digits it needs ("03:00:00-03:00"), as JSON views have since Grails 7. Jackson writes"03:00-03:00".HalJsonRenderer(application/hal+json) uses Groovy's default JSON generator and is untouched.grails.databinding.dateFormatsas before.These notes are in the upgrade guide.
Documentation
Monthbinding and the JSON views notes;java.sql.Timeand date map keys, and how to render aMonthas its number.Verification
JsonDateTimeRenderingSpec(converters) andDateTimeRenderingSpec(views, rendered through an actual view) compare Grails output withJsonMapper.builder().build().writeValueAsString(...).grails-web-common's test fixtures (DateTimeValues), including BC and 5-digit years and 15 map-key types.[d: java.sql.Date.valueOf('2026-09-25')] as JSON);javascriptdate mode and theJSONbuilder;java.sql.Timeand render aMonthas its number, including throughg.render.DateTimeHelperRenderingSpeccovers:g.renderof a domainMonth, a POGO and a map, compared with Jackson;timeZonecompared with Jackson'sdefaultTimeZone, includingAsia/KolkataandAsia/Kathmandu,dateFormatwithjava.sql.Time, and colliding date keys;ServiceLoader:g.renderstill renders that POGO property by property.MonthBindingSpecbinds9,"9"and"SEPTEMBER"throughGrailsWebDataBinder, and rejects13and9.7.Jsr310ConvertersConfigurationSpecchecks that a subclass written for Grails 7 (convert(value) { String format -> … }) reads only the configured formats.DateConversionHelperSpecpins the Grails 7 result for each value no format reads all of, with the default formats inAmerica/Denver.DateTimeRoundTripBindingSpecrenders values withas JSONand binds them back through a controller, with the JVM inAmerica/Denver.Date, dates before 1582, anOffsetDateTime, aZonedDateTime, aLocalDateTimewith a fraction and ajava.sql.Time.grails-web-common116,grails-converters194,grails-views-gson266,grails-views-core2;grails-databinding-core92,grails-databinding49,grails-web-databinding58;grails-test-suite-web443,grails-test-suite-persistence106.codeStylepasses forgrails-web-common,grails-converters,grails-views-gson,grails-databinding-coreandgrails-databinding.8.0.0-SNAPSHOTwithgrails-web-common,grails-converters,grails-views-gson,grails-databinding-coreandgrails-databindingfrom this branch returns JSON byte-identical to a Spring Boot 4.1.1 app, through bothrespondand a JSON view usingg.render, for every date and time field butmonth, which renders by its name.