You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/09/28 12:58:20 UTC

(grails-core) branch refactor/16411-marshaller-dedup updated (28e6a00c3b -> e21f0346fa)

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

borinquenkid pushed a change to branch refactor/16411-marshaller-dedup
in repository https://gitbox.apache.org/repos/asf/grails-core.git


 discard 28e6a00c3b Add direct unit tests for SimpleTypeMarshaller and SimpleTypeJsonConverter
 discard 3f54f28595 Collapse the remaining pre-existing single-type date/time marshallers
 discard 6d816ef9e4 Replace per-type date/time marshaller boilerplate with a generic one
     new e21f0346fa Collapse only the PR-B-safe single-type date/time marshallers

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (28e6a00c3b)
            \
             N -- N -- N   refs/heads/refactor/16411-marshaller-dedup (e21f0346fa)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../ConvertersConfigurationInitializer.java        |  6 ++----
 ...iptDateMarshaller.java => MonthMarshaller.java} | 14 +++++++------
 ...tDateMarshaller.java => SqlTimeMarshaller.java} | 15 ++++++++------
 .../json/converters/MonthJsonConverter.groovy      | 21 ++++++++++----------
 .../json/converters/OffsetTimeJsonConverter.groovy | 21 ++++++++++----------
 .../json/converters/SqlTimeJsonConverter.groovy    | 23 +++++++++++-----------
 .../plugin/json/view/JsonViewTemplateEngine.groovy | 12 +++++------
 7 files changed, 56 insertions(+), 56 deletions(-)
 copy grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/{JavascriptDateMarshaller.java => MonthMarshaller.java} (74%)
 copy grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/{JavascriptDateMarshaller.java => SqlTimeMarshaller.java} (71%)
 copy grails-test-examples/hibernate7/views-functional-tests/src/main/groovy/functional/tests/MyConverter.groovy => grails-views-gson/src/main/groovy/grails/plugin/json/converters/MonthJsonConverter.groovy (73%)
 copy grails-test-examples/views-functional-tests/src/main/groovy/functional/tests/MyConverter.groovy => grails-views-gson/src/main/groovy/grails/plugin/json/converters/OffsetTimeJsonConverter.groovy (72%)
 copy grails-test-examples/views-functional-tests/src/main/groovy/functional/tests/MyOtherConverter.groovy => grails-views-gson/src/main/groovy/grails/plugin/json/converters/SqlTimeJsonConverter.groovy (69%)


(grails-core) 01/01: Collapse only the PR-B-safe single-type date/time marshallers

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch refactor/16411-marshaller-dedup
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit e21f0346fa271d6fc5aa1efd6271ab3e681a22b4
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Mon Sep 28 07:51:02 2026 -0500

    Collapse only the PR-B-safe single-type date/time marshallers
    
    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 <no...@anthropic.com>
---
 .../ConvertersConfigurationInitializer.java        | 52 +++++++++----
 .../marshaller/json/DurationMarshaller.java        | 48 ------------
 .../marshaller/json/InstantMarshaller.java         | 49 ------------
 .../marshaller/json/LocalDateMarshaller.java       | 49 ------------
 .../marshaller/json/LocalDateTimeMarshaller.java   | 49 ------------
 .../marshaller/json/MonthDayMarshaller.java        | 48 ------------
 .../marshaller/json/OffsetDateTimeMarshaller.java  | 49 ------------
 .../marshaller/json/OffsetTimeMarshaller.java      | 48 ------------
 .../marshaller/json/PeriodMarshaller.java          | 48 ------------
 ...meMarshaller.java => SimpleTypeMarshaller.java} | 31 ++++++--
 .../marshaller/json/TimeZoneMarshaller.java        | 48 ------------
 .../json/XMLGregorianCalendarMarshaller.java       | 51 ------------
 .../marshaller/json/XmlDurationMarshaller.java     | 48 ------------
 .../converters/marshaller/json/YearMarshaller.java | 48 ------------
 .../marshaller/json/YearMonthMarshaller.java       | 48 ------------
 .../marshaller/json/ZoneIdMarshaller.java          | 48 ------------
 .../marshaller/json/ZonedDateTimeMarshaller.java   | 49 ------------
 .../json/SimpleTypeMarshallerSpec.groovy           | 91 ++++++++++++++++++++++
 .../json/converters/InstantJsonConverter.groovy    | 45 -----------
 .../json/converters/LocalDateJsonConverter.groovy  | 45 -----------
 .../converters/LocalDateTimeJsonConverter.groovy   | 45 -----------
 .../json/converters/LocalTimeJsonConverter.groovy  | 45 -----------
 .../json/converters/MonthDayJsonConverter.groovy   | 45 -----------
 .../converters/OffsetDateTimeJsonConverter.groovy  | 45 -----------
 .../json/converters/PeriodJsonConverter.groovy     | 44 -----------
 ...erter.groovy => SimpleTypeJsonConverter.groovy} | 26 +++++--
 .../json/converters/TimeZoneJsonConverter.groovy   | 43 ----------
 .../XMLGregorianCalendarJsonConverter.groovy       | 45 -----------
 .../converters/XmlDurationJsonConverter.groovy     | 45 -----------
 .../json/converters/YearJsonConverter.groovy       | 45 -----------
 .../json/converters/YearMonthJsonConverter.groovy  | 45 -----------
 .../json/converters/ZoneIdJsonConverter.groovy     | 45 -----------
 .../converters/ZonedDateTimeJsonConverter.groovy   | 45 -----------
 .../plugin/json/view/JsonViewTemplateEngine.groovy | 63 ++++++++-------
 .../converters/SimpleTypeJsonConverterSpec.groovy  | 60 ++++++++++++++
 35 files changed, 264 insertions(+), 1414 deletions(-)

diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/configuration/ConvertersConfigurationInitializer.java b/grails-converters/src/main/groovy/org/grails/web/converters/configuration/ConvertersConfigurationInitializer.java
index 7c1db42ab0..a94a415e32 100644
--- a/grails-converters/src/main/groovy/org/grails/web/converters/configuration/ConvertersConfigurationInitializer.java
+++ b/grails-converters/src/main/groovy/org/grails/web/converters/configuration/ConvertersConfigurationInitializer.java
@@ -18,9 +18,26 @@
  */
 package org.grails.web.converters.configuration;
 
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.MonthDay;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.Period;
+import java.time.Year;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.TimeZone;
+
+import javax.xml.datatype.XMLGregorianCalendar;
 
 import io.micrometer.observation.ObservationRegistry;
 import org.apache.commons.logging.Log;
@@ -41,6 +58,7 @@ import org.grails.config.PropertySourcesConfig;
 import org.grails.web.converters.Converter;
 import org.grails.web.converters.marshaller.ObjectMarshaller;
 import org.grails.web.converters.marshaller.ProxyUnwrappingMarshaller;
+import org.grails.web.json.JsonDateFormat;
 
 /**
  * @author Siegfried Puchbauer
@@ -123,22 +141,24 @@ public class ConvertersConfigurationInitializer implements ApplicationContextAwa
             marshallers.add(new org.grails.web.converters.marshaller.json.DateMarshaller());
         }
         marshallers.add(new org.grails.web.converters.marshaller.json.CalendarMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.XMLGregorianCalendarMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.InstantMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.LocalDateMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.LocalDateTimeMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.LocalTimeMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.OffsetDateTimeMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.OffsetTimeMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.ZonedDateTimeMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.YearMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.YearMonthMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.MonthDayMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.DurationMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.PeriodMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.ZoneIdMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.TimeZoneMarshaller());
-        marshallers.add(new org.grails.web.converters.marshaller.json.XmlDurationMarshaller());
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(XMLGregorianCalendar.class,
+                calendar -> JsonDateFormat.format(calendar.toGregorianCalendar().getTimeInMillis())));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(Instant.class, DateTimeFormatter.ISO_INSTANT::format));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(LocalDate.class, DateTimeFormatter.ISO_LOCAL_DATE::format));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(LocalDateTime.class, DateTimeFormatter.ISO_LOCAL_DATE_TIME::format));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(LocalTime.class, DateTimeFormatter.ISO_LOCAL_TIME::format));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME::format));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(OffsetTime.class, OffsetTime::toString));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(ZonedDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME::format));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(Year.class, Year::getValue));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(YearMonth.class, YearMonth::toString));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(MonthDay.class, MonthDay::toString));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(Duration.class, Duration::toString));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(Period.class, Period::toString));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(ZoneId.class, ZoneId::getId));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(TimeZone.class, TimeZone::getID));
+        marshallers.add(new org.grails.web.converters.marshaller.json.SimpleTypeMarshaller<>(javax.xml.datatype.Duration.class,
+                javax.xml.datatype.Duration::toString));
         marshallers.add(new org.grails.web.converters.marshaller.json.ToStringBeanMarshaller());
 
         boolean includeDomainVersion = includeDomainVersionProperty(grailsConfig, "json");
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/DurationMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/DurationMarshaller.java
deleted file mode 100644
index 8301a1a822..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/DurationMarshaller.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.Duration;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a Duration to its ISO-8601 {@link Duration#toString()} form
- * (e.g. {@code PT1H30M}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-public class DurationMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof Duration;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            converter.getWriter().value(object.toString());
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/InstantMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/InstantMarshaller.java
deleted file mode 100644
index 15b2e7ef90..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/InstantMarshaller.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.Instant;
-import java.time.format.DateTimeFormatter;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts an Instant to ISO-8601 format with Z suffix.
- *
- * @since 7.0
- */
-public class InstantMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof Instant;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            Instant instant = (Instant) object;
-            converter.getWriter().value(DateTimeFormatter.ISO_INSTANT.format(instant));
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/LocalDateMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/LocalDateMarshaller.java
deleted file mode 100644
index 5e669f02bd..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/LocalDateMarshaller.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a LocalDate to ISO-8601 date format (YYYY-MM-DD).
- *
- * @since 7.0
- */
-public class LocalDateMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof LocalDate;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            LocalDate localDate = (LocalDate) object;
-            converter.getWriter().value(DateTimeFormatter.ISO_LOCAL_DATE.format(localDate));
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/LocalDateTimeMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/LocalDateTimeMarshaller.java
deleted file mode 100644
index e03d8bdcdd..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/LocalDateTimeMarshaller.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a LocalDateTime to ISO-8601 format (without timezone).
- *
- * @since 7.0
- */
-public class LocalDateTimeMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof LocalDateTime;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            LocalDateTime localDateTime = (LocalDateTime) object;
-            converter.getWriter().value(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime));
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/MonthDayMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/MonthDayMarshaller.java
deleted file mode 100644
index 4c140ed354..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/MonthDayMarshaller.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.MonthDay;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a MonthDay to its ISO-8601 {@link MonthDay#toString()} form
- * (e.g. {@code --09-25}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-public class MonthDayMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof MonthDay;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            converter.getWriter().value(object.toString());
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/OffsetDateTimeMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/OffsetDateTimeMarshaller.java
deleted file mode 100644
index 83c84d680a..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/OffsetDateTimeMarshaller.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.OffsetDateTime;
-import java.time.format.DateTimeFormatter;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts an OffsetDateTime to ISO-8601 format with timezone offset.
- *
- * @since 7.0
- */
-public class OffsetDateTimeMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof OffsetDateTime;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            OffsetDateTime offsetDateTime = (OffsetDateTime) object;
-            converter.getWriter().value(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(offsetDateTime));
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/OffsetTimeMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/OffsetTimeMarshaller.java
deleted file mode 100644
index 7cc1a78521..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/OffsetTimeMarshaller.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.OffsetTime;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts an OffsetTime to its ISO-8601 {@link OffsetTime#toString()} form
- * (e.g. {@code 03:00-03:00}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-public class OffsetTimeMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof OffsetTime;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            converter.getWriter().value(object.toString());
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/PeriodMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/PeriodMarshaller.java
deleted file mode 100644
index 3938e3c85f..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/PeriodMarshaller.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.Period;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a Period to its ISO-8601 {@link Period#toString()} form
- * (e.g. {@code P1Y2M3D}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-public class PeriodMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof Period;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            converter.getWriter().value(object.toString());
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/LocalTimeMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/SimpleTypeMarshaller.java
similarity index 56%
rename from grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/LocalTimeMarshaller.java
rename to grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/SimpleTypeMarshaller.java
index 27a3113bb1..ce01e7079a 100644
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/LocalTimeMarshaller.java
+++ b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/SimpleTypeMarshaller.java
@@ -18,8 +18,7 @@
  */
 package org.grails.web.converters.marshaller.json;
 
-import java.time.LocalTime;
-import java.time.format.DateTimeFormatter;
+import java.util.function.Function;
 
 import grails.converters.JSON;
 import org.grails.web.converters.exceptions.ConverterException;
@@ -27,23 +26,41 @@ import org.grails.web.converters.marshaller.ObjectMarshaller;
 import org.grails.web.json.JSONException;
 
 /**
- * JSON ObjectMarshaller which converts a LocalTime to ISO-8601 time format (HH:mm:ss[.fraction]).
+ * A JSON {@link ObjectMarshaller} for a type whose JSON representation is a single {@link String} or
+ * {@link Number} value computed directly from the object, such as {@code toString()} or a single
+ * accessor. Registered once per type in {@code ConvertersConfigurationInitializer} in place of a
+ * dedicated marshaller class.
  *
+ * @param <T> the supported type
  * @since 8.0
  */
-public class LocalTimeMarshaller implements ObjectMarshaller<JSON> {
+public final class SimpleTypeMarshaller<T> implements ObjectMarshaller<JSON> {
+
+    private final Class<T> type;
+
+    private final Function<T, Object> valueExtractor;
+
+    /**
+     * @param type the type this marshaller supports
+     * @param valueExtractor computes the JSON value (a {@link String} or a {@link Number}) from an
+     *        instance of {@code type}
+     */
+    public SimpleTypeMarshaller(Class<T> type, Function<T, Object> valueExtractor) {
+        this.type = type;
+        this.valueExtractor = valueExtractor;
+    }
 
     public boolean supports(Object object) {
-        return object instanceof LocalTime;
+        return type.isInstance(object);
     }
 
     public void marshalObject(Object object, JSON converter) throws ConverterException {
         try {
-            LocalTime localTime = (LocalTime) object;
-            converter.getWriter().value(DateTimeFormatter.ISO_LOCAL_TIME.format(localTime));
+            converter.getWriter().value(valueExtractor.apply(type.cast(object)));
         }
         catch (JSONException e) {
             throw new ConverterException(e);
         }
     }
+
 }
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/TimeZoneMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/TimeZoneMarshaller.java
deleted file mode 100644
index 29480ee77a..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/TimeZoneMarshaller.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.util.TimeZone;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a TimeZone to its ID (e.g. {@code America/Sao_Paulo}),
- * the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-public class TimeZoneMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof TimeZone;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            converter.getWriter().value(((TimeZone) object).getID());
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/XMLGregorianCalendarMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/XMLGregorianCalendarMarshaller.java
deleted file mode 100644
index 577a1f6719..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/XMLGregorianCalendarMarshaller.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import javax.xml.datatype.XMLGregorianCalendar;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-import org.grails.web.json.JsonDateFormat;
-
-/**
- * JSON ObjectMarshaller which converts an XMLGregorianCalendar to an RFC 3339 / ISO 8601 UTC instant
- * string with millisecond precision, as {@link CalendarMarshaller} does for a Calendar, the same as
- * Spring Boot's default Jackson rendering. See {@link JsonDateFormat}.
- *
- * @since 8.0
- */
-public class XMLGregorianCalendarMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof XMLGregorianCalendar;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            long epochMillis = ((XMLGregorianCalendar) object).toGregorianCalendar().getTimeInMillis();
-            converter.getWriter().value(JsonDateFormat.format(epochMillis));
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/XmlDurationMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/XmlDurationMarshaller.java
deleted file mode 100644
index 8805b57916..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/XmlDurationMarshaller.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import javax.xml.datatype.Duration;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a {@link javax.xml.datatype.Duration} to its lexical
- * {@link Duration#toString()} form (e.g. {@code P1DT2H}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-public class XmlDurationMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof Duration;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            converter.getWriter().value(object.toString());
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/YearMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/YearMarshaller.java
deleted file mode 100644
index 9d07a79ac9..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/YearMarshaller.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.Year;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a Year to its number (e.g. {@code 2026}), the same as
- * Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-public class YearMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof Year;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            converter.getWriter().value(((Year) object).getValue());
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/YearMonthMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/YearMonthMarshaller.java
deleted file mode 100644
index 5908c71742..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/YearMonthMarshaller.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.YearMonth;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a YearMonth to its ISO-8601 {@link YearMonth#toString()} form
- * (e.g. {@code 2026-09}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-public class YearMonthMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof YearMonth;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            converter.getWriter().value(object.toString());
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/ZoneIdMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/ZoneIdMarshaller.java
deleted file mode 100644
index 25ca43fb24..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/ZoneIdMarshaller.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.ZoneId;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a ZoneId, including a ZoneOffset, to its ID
- * (e.g. {@code America/Sao_Paulo} or {@code -03:00}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-public class ZoneIdMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof ZoneId;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            converter.getWriter().value(((ZoneId) object).getId());
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/ZonedDateTimeMarshaller.java b/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/ZonedDateTimeMarshaller.java
deleted file mode 100644
index d65b420105..0000000000
--- a/grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/ZonedDateTimeMarshaller.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.grails.web.converters.marshaller.json;
-
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-
-import grails.converters.JSON;
-import org.grails.web.converters.exceptions.ConverterException;
-import org.grails.web.converters.marshaller.ObjectMarshaller;
-import org.grails.web.json.JSONException;
-
-/**
- * JSON ObjectMarshaller which converts a ZonedDateTime to ISO-8601 format with timezone offset.
- *
- * @since 7.0
- */
-public class ZonedDateTimeMarshaller implements ObjectMarshaller<JSON> {
-
-    public boolean supports(Object object) {
-        return object instanceof ZonedDateTime;
-    }
-
-    public void marshalObject(Object object, JSON converter) throws ConverterException {
-        try {
-            ZonedDateTime zonedDateTime = (ZonedDateTime) object;
-            converter.getWriter().value(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zonedDateTime));
-        }
-        catch (JSONException e) {
-            throw new ConverterException(e);
-        }
-    }
-}
diff --git a/grails-converters/src/test/groovy/org/grails/web/converters/marshaller/json/SimpleTypeMarshallerSpec.groovy b/grails-converters/src/test/groovy/org/grails/web/converters/marshaller/json/SimpleTypeMarshallerSpec.groovy
new file mode 100644
index 0000000000..d9357559d0
--- /dev/null
+++ b/grails-converters/src/test/groovy/org/grails/web/converters/marshaller/json/SimpleTypeMarshallerSpec.groovy
@@ -0,0 +1,91 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.web.converters.marshaller.json
+
+import java.time.Year
+
+import spock.lang.Specification
+
+import grails.converters.JSON
+import org.grails.web.json.JSONWriter
+
+class SimpleTypeMarshallerSpec extends Specification {
+
+    void "supports returns true only for instances of the given type, including subtypes"() {
+        given:
+        def marshaller = new SimpleTypeMarshaller<Number>(Number.class, { it })
+
+        expect:
+        with(marshaller) {
+            supports(42)
+            supports(42L)
+            supports(3.14d)
+            !supports('42')
+            !supports(null)
+        }
+    }
+
+    void "marshalObject writes a String value extractor result quoted"() {
+        given:
+        def marshaller = new SimpleTypeMarshaller<Year>(Year.class, { Year year -> year.toString() })
+
+        when:
+        def result = marshalToString(marshaller, Year.of(2026))
+
+        then:
+        result == '["2026"]'
+    }
+
+    void "marshalObject writes a Number value extractor result unquoted"() {
+        given:
+        def marshaller = new SimpleTypeMarshaller<Year>(Year.class, { Year year -> year.getValue() })
+
+        when:
+        def result = marshalToString(marshaller, Year.of(2026))
+
+        then:
+        result == '[2026]'
+    }
+
+    void "marshalObject passes the object to the extractor cast to the declared type"() {
+        given:
+        Class<?> extractorReceivedType = null
+        def marshaller = new SimpleTypeMarshaller<Year>(Year.class, { Year year ->
+            extractorReceivedType = year.class
+            year.toString()
+        })
+
+        when:
+        marshalToString(marshaller, Year.of(2026))
+
+        then:
+        extractorReceivedType == Year
+    }
+
+    private static String marshalToString(SimpleTypeMarshaller marshaller, Object value) {
+        def json = new JSON()
+        def stringWriter = new StringWriter()
+        json.writer = new JSONWriter(stringWriter)
+        json.writer.array()
+        marshaller.marshalObject(value, json)
+        json.writer.endArray()
+        stringWriter.toString()
+    }
+
+}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/InstantJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/InstantJsonConverter.groovy
deleted file mode 100644
index 15411f5714..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/InstantJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.Instant
-import java.time.format.DateTimeFormatter
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link java.time.Instant} as json
- *
- * @author James Kleeh
- */
-@CompileStatic
-class InstantJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        Instant == type
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        DateTimeFormatter.ISO_INSTANT.format((Instant) value)
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/LocalDateJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/LocalDateJsonConverter.groovy
deleted file mode 100644
index 72b565e3cd..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/LocalDateJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.LocalDate
-import java.time.format.DateTimeFormatter
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link LocalDate} as json
- *
- * @author James Kleeh
- */
-@CompileStatic
-class LocalDateJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        LocalDate == type
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        DateTimeFormatter.ISO_LOCAL_DATE.format((LocalDate) value)
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/LocalDateTimeJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/LocalDateTimeJsonConverter.groovy
deleted file mode 100644
index b8448d26a7..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/LocalDateTimeJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.LocalDateTime
-import java.time.format.DateTimeFormatter
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link LocalDateTime} as json
- *
- * @author James Kleeh
- */
-@CompileStatic
-class LocalDateTimeJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        LocalDateTime == type
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        DateTimeFormatter.ISO_LOCAL_DATE_TIME.format((LocalDateTime) value)
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/LocalTimeJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/LocalTimeJsonConverter.groovy
deleted file mode 100644
index a98a0726a7..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/LocalTimeJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.LocalTime
-import java.time.format.DateTimeFormatter
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link LocalTime} as json
- *
- * @author James Kleeh
- */
-@CompileStatic
-class LocalTimeJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        LocalTime == type
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        DateTimeFormatter.ISO_LOCAL_TIME.format((LocalTime) value)
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/MonthDayJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/MonthDayJsonConverter.groovy
deleted file mode 100644
index cc50b82f2d..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/MonthDayJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.MonthDay
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link MonthDay} as json: its ISO-8601 {@link MonthDay#toString()} form
- * (e.g. {@code --09-25}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-@CompileStatic
-class MonthDayJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        MonthDay == type
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        value.toString()
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/OffsetDateTimeJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/OffsetDateTimeJsonConverter.groovy
deleted file mode 100644
index ad64b89227..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/OffsetDateTimeJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.OffsetDateTime
-import java.time.format.DateTimeFormatter
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link OffsetDateTime} as json
- *
- * @author James Kleeh
- */
-@CompileStatic
-class OffsetDateTimeJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        OffsetDateTime == type
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        DateTimeFormatter.ISO_OFFSET_DATE_TIME.format((OffsetDateTime) value)
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/PeriodJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/PeriodJsonConverter.groovy
deleted file mode 100644
index fc006eeb15..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/PeriodJsonConverter.groovy
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.Period
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link Period} as json
- *
- * @author Muhammad Hamza Zaib
- */
-@CompileStatic
-class PeriodJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        Period == type
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        ((Period) value).toString()
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/DurationJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/SimpleTypeJsonConverter.groovy
similarity index 55%
rename from grails-views-gson/src/main/groovy/grails/plugin/json/converters/DurationJsonConverter.groovy
rename to grails-views-gson/src/main/groovy/grails/plugin/json/converters/SimpleTypeJsonConverter.groovy
index fea5e18067..b0c49af952 100644
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/DurationJsonConverter.groovy
+++ b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/SimpleTypeJsonConverter.groovy
@@ -19,27 +19,41 @@
 
 package grails.plugin.json.converters
 
-import java.time.Duration
+import java.util.function.Function
 
 import groovy.json.JsonGenerator
 import groovy.transform.CompileStatic
 
 /**
- * A class to render a {@link Duration} as json: its ISO-8601 {@link Duration#toString()} form
- * (e.g. {@code PT1H30M}), the same as Spring Boot's default Jackson rendering.
+ * A {@link JsonGenerator.Converter} for a type whose JSON representation is a single value computed
+ * directly from the object, such as {@code toString()} or a single accessor. Registered once per type
+ * in {@code JsonViewTemplateEngine} in place of a dedicated converter class.
  *
  * @since 8.0
  */
 @CompileStatic
-class DurationJsonConverter implements JsonGenerator.Converter {
+class SimpleTypeJsonConverter implements JsonGenerator.Converter {
+
+    private final Class<?> type
+    private final Function<Object, Object> valueExtractor
+
+    /**
+     * @param type the type this converter handles, including its subtypes
+     * @param valueExtractor computes the JSON value from an instance of {@code type}
+     */
+    SimpleTypeJsonConverter(Class<?> type, Function<Object, Object> valueExtractor) {
+        this.type = type
+        this.valueExtractor = valueExtractor
+    }
 
     @Override
     boolean handles(Class<?> type) {
-        Duration == type
+        this.type.isAssignableFrom(type)
     }
 
     @Override
     Object convert(Object value, String key) {
-        value.toString()
+        valueExtractor.apply(value)
     }
+
 }
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/TimeZoneJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/TimeZoneJsonConverter.groovy
deleted file mode 100644
index 4c62b5c61a..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/TimeZoneJsonConverter.groovy
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link TimeZone} as json: its ID (e.g. {@code America/Sao_Paulo}), the same as
- * Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-@CompileStatic
-class TimeZoneJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        TimeZone.isAssignableFrom(type)
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        ((TimeZone) value).ID
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/XMLGregorianCalendarJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/XMLGregorianCalendarJsonConverter.groovy
deleted file mode 100644
index 1cae24391e..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/XMLGregorianCalendarJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import javax.xml.datatype.XMLGregorianCalendar
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render an {@link XMLGregorianCalendar} as json: it is converted to a {@link GregorianCalendar},
- * which is written as any other {@link Calendar} is, the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-@CompileStatic
-class XMLGregorianCalendarJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        XMLGregorianCalendar.isAssignableFrom(type)
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        ((XMLGregorianCalendar) value).toGregorianCalendar()
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/XmlDurationJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/XmlDurationJsonConverter.groovy
deleted file mode 100644
index 37a01941ec..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/XmlDurationJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import javax.xml.datatype.Duration
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link javax.xml.datatype.Duration} as json: its lexical {@link Duration#toString()}
- * form (e.g. {@code P1DT2H}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-@CompileStatic
-class XmlDurationJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        Duration.isAssignableFrom(type)
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        value.toString()
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/YearJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/YearJsonConverter.groovy
deleted file mode 100644
index aa58a98906..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/YearJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.Year
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link Year} as json: its number (e.g. {@code 2026}), the same as Spring Boot's
- * default Jackson rendering.
- *
- * @since 8.0
- */
-@CompileStatic
-class YearJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        Year == type
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        ((Year) value).value
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/YearMonthJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/YearMonthJsonConverter.groovy
deleted file mode 100644
index c8c6932c34..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/YearMonthJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.YearMonth
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link YearMonth} as json: its ISO-8601 {@link YearMonth#toString()} form
- * (e.g. {@code 2026-09}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-@CompileStatic
-class YearMonthJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        YearMonth == type
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        value.toString()
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/ZoneIdJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/ZoneIdJsonConverter.groovy
deleted file mode 100644
index 428f3dac33..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/ZoneIdJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.ZoneId
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link ZoneId}, including a {@link java.time.ZoneOffset}, as json: its ID
- * (e.g. {@code America/Sao_Paulo} or {@code -03:00}), the same as Spring Boot's default Jackson rendering.
- *
- * @since 8.0
- */
-@CompileStatic
-class ZoneIdJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        ZoneId.isAssignableFrom(type)
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        ((ZoneId) value).id
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/ZonedDateTimeJsonConverter.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/converters/ZonedDateTimeJsonConverter.groovy
deleted file mode 100644
index 49eb0381df..0000000000
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/converters/ZonedDateTimeJsonConverter.groovy
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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 grails.plugin.json.converters
-
-import java.time.ZonedDateTime
-import java.time.format.DateTimeFormatter
-
-import groovy.json.JsonGenerator
-import groovy.transform.CompileStatic
-
-/**
- * A class to render a {@link ZonedDateTime} as json
- *
- * @author James Kleeh
- */
-@CompileStatic
-class ZonedDateTimeJsonConverter implements JsonGenerator.Converter {
-
-    @Override
-    boolean handles(Class<?> type) {
-        ZonedDateTime == type
-    }
-
-    @Override
-    Object convert(Object value, String key) {
-        DateTimeFormatter.ISO_OFFSET_DATE_TIME.format((ZonedDateTime) value)
-    }
-}
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/view/JsonViewTemplateEngine.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/view/JsonViewTemplateEngine.groovy
index 80c6998be3..e620cfd523 100644
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/view/JsonViewTemplateEngine.groovy
+++ b/grails-views-gson/src/main/groovy/grails/plugin/json/view/JsonViewTemplateEngine.groovy
@@ -19,6 +19,23 @@
 
 package grails.plugin.json.view
 
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.MonthDay
+import java.time.OffsetDateTime
+import java.time.Period
+import java.time.Year
+import java.time.YearMonth
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.util.function.Function
+
+import javax.xml.datatype.XMLGregorianCalendar
+
 import groovy.json.JsonGenerator
 import groovy.text.Template
 import groovy.transform.CompileStatic
@@ -28,24 +45,10 @@ import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.core.OrderComparator
 
-import grails.plugin.json.converters.DurationJsonConverter
-import grails.plugin.json.converters.InstantJsonConverter
-import grails.plugin.json.converters.LocalDateJsonConverter
-import grails.plugin.json.converters.LocalDateTimeJsonConverter
-import grails.plugin.json.converters.LocalTimeJsonConverter
-import grails.plugin.json.converters.MonthDayJsonConverter
 import grails.plugin.json.converters.MonthJsonConverter
-import grails.plugin.json.converters.OffsetDateTimeJsonConverter
 import grails.plugin.json.converters.OffsetTimeJsonConverter
-import grails.plugin.json.converters.PeriodJsonConverter
+import grails.plugin.json.converters.SimpleTypeJsonConverter
 import grails.plugin.json.converters.SqlTimeJsonConverter
-import grails.plugin.json.converters.TimeZoneJsonConverter
-import grails.plugin.json.converters.XMLGregorianCalendarJsonConverter
-import grails.plugin.json.converters.XmlDurationJsonConverter
-import grails.plugin.json.converters.YearJsonConverter
-import grails.plugin.json.converters.YearMonthJsonConverter
-import grails.plugin.json.converters.ZoneIdJsonConverter
-import grails.plugin.json.converters.ZonedDateTimeJsonConverter
 import grails.plugin.json.view.api.jsonapi.JsonApiIdRenderStrategy
 import grails.plugin.json.view.internal.JsonTemplateTypeCheckingExtension
 import grails.plugin.json.view.internal.JsonViewsTransform
@@ -117,24 +120,24 @@ class JsonViewTemplateEngine extends ResolvableGroovyTemplateEngine {
         Map<String, JsonGenerator.Converter> convertersByClass = new LinkedHashMap<>()
         registerConverters(ServiceLoader.load(JsonGenerator.Converter, classLoader), convertersByClass)
         List<JsonGenerator.Converter> converters = new ArrayList<>(convertersByClass.values())
-        converters.add(new InstantJsonConverter())
-        converters.add(new LocalDateJsonConverter())
-        converters.add(new LocalDateTimeJsonConverter())
-        converters.add(new LocalTimeJsonConverter())
-        converters.add(new OffsetDateTimeJsonConverter())
+        converters.add(new SimpleTypeJsonConverter(Instant, { DateTimeFormatter.ISO_INSTANT.format((Instant) it) } as Function))
+        converters.add(new SimpleTypeJsonConverter(LocalDate, { DateTimeFormatter.ISO_LOCAL_DATE.format((LocalDate) it) } as Function))
+        converters.add(new SimpleTypeJsonConverter(LocalDateTime, { DateTimeFormatter.ISO_LOCAL_DATE_TIME.format((LocalDateTime) it) } as Function))
+        converters.add(new SimpleTypeJsonConverter(LocalTime, { DateTimeFormatter.ISO_LOCAL_TIME.format((LocalTime) it) } as Function))
+        converters.add(new SimpleTypeJsonConverter(OffsetDateTime, { DateTimeFormatter.ISO_OFFSET_DATE_TIME.format((OffsetDateTime) it) } as Function))
         converters.add(new OffsetTimeJsonConverter())
-        converters.add(new PeriodJsonConverter())
+        converters.add(new SimpleTypeJsonConverter(Period, { it.toString() } as Function))
         converters.add(new SqlTimeJsonConverter())
-        converters.add(new ZonedDateTimeJsonConverter())
-        converters.add(new YearJsonConverter())
-        converters.add(new YearMonthJsonConverter())
-        converters.add(new MonthDayJsonConverter())
+        converters.add(new SimpleTypeJsonConverter(ZonedDateTime, { DateTimeFormatter.ISO_OFFSET_DATE_TIME.format((ZonedDateTime) it) } as Function))
+        converters.add(new SimpleTypeJsonConverter(Year, { ((Year) it).value } as Function))
+        converters.add(new SimpleTypeJsonConverter(YearMonth, { it.toString() } as Function))
+        converters.add(new SimpleTypeJsonConverter(MonthDay, { it.toString() } as Function))
         converters.add(new MonthJsonConverter())
-        converters.add(new DurationJsonConverter())
-        converters.add(new ZoneIdJsonConverter())
-        converters.add(new TimeZoneJsonConverter())
-        converters.add(new XMLGregorianCalendarJsonConverter())
-        converters.add(new XmlDurationJsonConverter())
+        converters.add(new SimpleTypeJsonConverter(Duration, { it.toString() } as Function))
+        converters.add(new SimpleTypeJsonConverter(ZoneId, { ((ZoneId) it).id } as Function))
+        converters.add(new SimpleTypeJsonConverter(TimeZone, { ((TimeZone) it).ID } as Function))
+        converters.add(new SimpleTypeJsonConverter(XMLGregorianCalendar, { ((XMLGregorianCalendar) it).toGregorianCalendar() } as Function))
+        converters.add(new SimpleTypeJsonConverter(javax.xml.datatype.Duration, { it.toString() } as Function))
         OrderComparator.sort(converters)
         converters.each {
             options.addConverter(it)
diff --git a/grails-views-gson/src/test/groovy/grails/plugin/json/converters/SimpleTypeJsonConverterSpec.groovy b/grails-views-gson/src/test/groovy/grails/plugin/json/converters/SimpleTypeJsonConverterSpec.groovy
new file mode 100644
index 0000000000..3527b0e402
--- /dev/null
+++ b/grails-views-gson/src/test/groovy/grails/plugin/json/converters/SimpleTypeJsonConverterSpec.groovy
@@ -0,0 +1,60 @@
+/*
+ *  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 grails.plugin.json.converters
+
+import java.time.Year
+import java.util.function.Function
+
+import spock.lang.Specification
+
+class SimpleTypeJsonConverterSpec extends Specification {
+
+    void "handles returns true for the given type and its subtypes"() {
+        given:
+        def converter = new SimpleTypeJsonConverter(Number, { it } as Function)
+
+        expect:
+        with(converter) {
+            handles(Integer)
+            handles(Long)
+            handles(Number)
+            !handles(String)
+        }
+    }
+
+    void "handles returns true only for an exact match when the type has no subtypes"() {
+        given:
+        def converter = new SimpleTypeJsonConverter(Year, { it } as Function)
+
+        expect:
+        with(converter) {
+            handles(Year)
+            !handles(Integer)
+        }
+    }
+
+    void "convert delegates to the value extractor"() {
+        given:
+        def converter = new SimpleTypeJsonConverter(Year, { ((Year) it).value } as Function)
+
+        expect:
+        converter.convert(Year.of(2026), 'year') == 2026
+    }
+
+}