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

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 293fc1495b Built-in support for marshalling date/time objects
293fc1495b is described below

commit 293fc1495b6269036880e83bf8ce3a7296093443
Author: James Bognar <[email protected]>
AuthorDate: Sat Feb 28 09:03:36 2026 -0500

    Built-in support for marshalling date/time objects
---
 TODO.md                                            |   3 +-
 .../main/java/org/apache/juneau/BeanSession.java   |  13 +
 .../src/main/java/org/apache/juneau/ClassMeta.java |  15 +-
 .../apache/juneau/csv/CsvSerializerSession.java    |  29 +-
 .../org/apache/juneau/html/HtmlParserSession.java  |   5 +
 .../apache/juneau/html/HtmlSerializerSession.java  |  17 +
 .../org/apache/juneau/json/JsonParserSession.java  |   3 +
 .../apache/juneau/json/JsonSerializerSession.java  |   5 +
 .../juneau/msgpack/MsgPackParserSession.java       |   3 +
 .../juneau/msgpack/MsgPackSerializerSession.java   |   5 +
 .../apache/juneau/oapi/OpenApiParserSession.java   |  31 +-
 .../juneau/oapi/OpenApiSerializerSession.java      |  28 +-
 .../org/apache/juneau/parser/ParserSession.java    |   3 +
 .../juneau/serializer/SerializerSession.java       |   4 +
 .../java/org/apache/juneau/swap/DefaultSwaps.java  |  16 -
 .../apache/juneau/swaps/TemporalCalendarSwap.java  |   5 +
 .../org/apache/juneau/swaps/TemporalDateSwap.java  |   5 +
 .../java/org/apache/juneau/swaps/TemporalSwap.java |   5 +
 .../juneau/swaps/XMLGregorianCalendarSwap.java     |   5 +
 .../org/apache/juneau/uon/UonParserSession.java    |   3 +
 .../apache/juneau/uon/UonSerializerSession.java    |   5 +
 .../java/org/apache/juneau/utils/Iso8601Utils.java | 313 ++++++++++++++++++
 .../org/apache/juneau/xml/XmlParserSession.java    |   3 +
 .../apache/juneau/xml/XmlSerializerSession.java    |   5 +
 .../juneau/a/rttests/RoundTripDateTime_Test.java   | 368 +++++++++++++++++++++
 .../BuiltInDateTimeSerialization_Test.java         | 313 ++++++++++++++++++
 .../juneau/transforms/DefaultSwaps_Test.java       |  30 ++
 27 files changed, 1166 insertions(+), 74 deletions(-)

diff --git a/TODO.md b/TODO.md
index 5b6f7e5210..4981320347 100644
--- a/TODO.md
+++ b/TODO.md
@@ -6,7 +6,6 @@
 - Find places where we defined fields and methods as _foobar and convert them 
to foobar_
 - Investigate navlinks URL generation issue: Either 
"request:?Accept=text/json&plainText=true" should be supported, or 
"request:/?Accept=text/json&plainText=true" should not append '/' to the 
request URL. Currently, "request:/?Accept=..." generates URLs like 
"http://localhost:5000/rest/db/request:?Accept=..."; which is incorrect.
 - Determine if it's possible to add a "short" field to @Schema for AI purposes.
-- Ensure Juneau support record types for serializing/parsing.
 - JsonSchemaParser should be able to produce JsonSchema beans.
 - JsonSchemaGenerator should return JsonSchema beans.
 - Create full-fledged CSV serializer/parser support.
@@ -15,5 +14,5 @@
 - ClassInfo should have a findGetter(String propertyName) convenience method.
 - Make sure @Beanp("*") works on plain fields.
 - Add schema validation to beans during parsing.
-- Duration objects should be supported for serialization by default.
+
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java
index d5cfd08d5e..0fc5b5a0ec 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanSession.java
@@ -41,6 +41,7 @@ import org.apache.juneau.commons.reflect.*;
 import org.apache.juneau.commons.time.*;
 import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.*;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
BeanContext}.
@@ -1214,6 +1215,18 @@ public class BeanSession extends ContextSession {
                                        return (T)swap.swap(this, value);
                        }
 
+                       if (to.isCharSequence() && 
(from.isDateOrCalendarOrTemporal() || from.isDuration()))
+                               return (T) Iso8601Utils.format(value, from, 
getTimeZone());
+
+                       if (to.isDateOrCalendarOrTemporal() && value instanceof 
CharSequence)
+                               return (T) Iso8601Utils.parse(value.toString(), 
to, getTimeZone());
+
+                       if (to.isDuration() && value instanceof CharSequence)
+                               return (T) Duration.parse(value.toString());
+
+                       if (to.isDateOrCalendarOrTemporal() && value instanceof 
Number)
+                               return (T) 
Iso8601Utils.fromEpochMillis(((Number)value).longValue(), to, getTimeZone());
+
                        if (to.isPrimitive()) {
                                if (to.isNumber()) {
                                        if (from.isNumber()) {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
index 42b94cb662..95f25d1384 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
@@ -27,6 +27,7 @@ import java.lang.annotation.*;
 import java.lang.reflect.*;
 import java.lang.reflect.Proxy;
 import java.net.*;
+import java.time.*;
 import java.time.temporal.*;
 import java.util.*;
 import java.util.List;
@@ -116,7 +117,8 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
                BEAN(20),
                ITERATOR(21),
                ITERABLE(22),
-               STREAM(23);
+               STREAM(23),
+               DURATION(24);
 
                private final int mask;
 
@@ -236,8 +238,12 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
                        } else if (isAssignableTo(Calendar.class)) {
                                cat.set(CALENDAR);
                        }
+               } else if (is(Duration.class)) {
+                       cat.set(DURATION);
                } else if (isAssignableTo(Temporal.class)) {
                        cat.set(TEMPORAL);
+               } else if 
(isAssignableTo(javax.xml.datatype.XMLGregorianCalendar.class)) {
+                       cat.set(CALENDAR);
                } else if (inner().isArray()) {
                        cat.set(ARRAY);
                } else if (isAssignableToAny(URL.class, URI.class) || 
ap.has(Uri.class, this)) {
@@ -1173,6 +1179,13 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
         */
        public boolean isTemporal() { return cat.is(TEMPORAL); }
 
+       /**
+        * Returns <jk>true</jk> if this class is a {@link java.time.Duration}.
+        *
+        * @return <jk>true</jk> if this class is a {@link java.time.Duration}.
+        */
+       public boolean isDuration() { return cat.is(DURATION); }
+
        /**
         * Returns <jk>true</jk> if this class is a {@link URI} or {@link URL}.
         *
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
index bf52621b77..f2e642319e 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
@@ -27,11 +27,17 @@ import java.nio.charset.*;
 import java.util.*;
 import java.util.function.*;
 
+import java.time.*;
+import java.time.temporal.*;
+import java.util.Calendar;
+import java.util.Date;
+
 import org.apache.juneau.*;
 import org.apache.juneau.commons.lang.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
CsvSerializer}.
@@ -216,6 +222,10 @@ public class CsvSerializerSession extends 
WriterSerializerSession {
                        if (nn(swap)) {
                                return swap(swap, value);
                        }
+                       if (type.isDateOrCalendarOrTemporal())
+                               return Iso8601Utils.format(value, type, 
getTimeZone());
+                       if (type.isDuration())
+                               return value.toString();
                        return value;
                } catch (SerializeException e) {
                        throw rex(e);
@@ -262,10 +272,10 @@ public class CsvSerializerSession extends 
WriterSerializerSession {
                                                var addComma2 = Flag.create();
                                                BeanMap<?> bean = toBeanMap(x);
                                                
bm.getProperties().values().stream().filter(BeanPropertyMeta::canRead).forEach(y
 -> {
-                                                       addComma2.ifSet(() -> 
w.w(',')).set();
-                                                       // Bean property values 
are already swapped by BeanPropertyMeta.get() via toSerializedForm()
-                                                       var value = y.get(bean, 
y.getName());
-                                                       w.writeEntry(value);
+                                               addComma2.ifSet(() -> 
w.w(',')).set();
+                                               var value = y.get(bean, 
y.getName());
+                                               value = 
formatIfDateOrDuration(value);
+                                               w.writeEntry(value);
                                                });
                                                w.w('\n');
                                        });
@@ -300,6 +310,17 @@ public class CsvSerializerSession extends 
WriterSerializerSession {
                }
        }
 
+       private Object formatIfDateOrDuration(Object value) {
+               if (value == null)
+                       return null;
+               if (value instanceof Calendar || value instanceof Date || value 
instanceof Temporal
+                               || value instanceof 
javax.xml.datatype.XMLGregorianCalendar)
+                       return Iso8601Utils.format(value, 
getClassMetaForObject(value), getTimeZone());
+               if (value instanceof Duration)
+                       return value.toString();
+               return value;
+       }
+
        CsvWriter getCsvWriter(SerializerPipe out) {
                var output = out.getRawOutput();
                if (output instanceof CsvWriter output2)
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlParserSession.java
index 3db9006c3b..65baa3916e 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlParserSession.java
@@ -39,6 +39,7 @@ import org.apache.juneau.html.annotation.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
 import org.apache.juneau.xml.*;
 
 /**
@@ -351,6 +352,8 @@ public class HtmlParserSession extends XmlParserSession {
                                o = Boolean.parseBoolean(text);
                        else if (sType.isNumber())
                                o = parseNumber(text, (Class<? extends 
Number>)eType.inner());
+                       else if (sType.isDateOrCalendarOrTemporal() || 
sType.isDuration())
+                               o = Iso8601Utils.parse(text, sType, 
getTimeZone());
                        else if (sType.canCreateNewInstanceFromString(outer))
                                o = sType.newInstanceFromString(outer, text);
                        else
@@ -362,6 +365,8 @@ public class HtmlParserSession extends XmlParserSession {
                                o = text;
                        else if (sType.isChar())
                                o = parseCharacter(text);
+                       else if (sType.isDateOrCalendarOrTemporal() || 
sType.isDuration())
+                               o = Iso8601Utils.parse(text, sType, 
getTimeZone());
                        else if (sType.canCreateNewInstanceFromString(outer))
                                o = sType.newInstanceFromString(outer, text);
                        else
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
index 72da37ad01..78ac1bdd46 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
@@ -38,6 +38,7 @@ import org.apache.juneau.httppart.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.svl.*;
 import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.*;
 import org.apache.juneau.xml.*;
 import org.apache.juneau.xml.annotation.*;
 
@@ -963,6 +964,22 @@ public class HtmlSerializerSession extends 
XmlSerializerSession {
                                        
out.sTag("boolean").append(o).eTag("boolean");
                                cr = CR_MIXED;
 
+                       } else if (sType.isDateOrCalendarOrTemporal()) {
+                               String s = Iso8601Utils.format(o, sType, 
getTimeZone());
+                               if (isRoot && addJsonTags)
+                                       
out.sTag("string").text(s).eTag("string");
+                               else
+                                       out.text(s);
+                               cr = CR_MIXED;
+
+                       } else if (sType.isDuration()) {
+                               String s = o.toString();
+                               if (isRoot && addJsonTags)
+                                       
out.sTag("string").text(s).eTag("string");
+                               else
+                                       out.text(s);
+                               cr = CR_MIXED;
+
                        } else if (sType.isMap() || (nn(wType) && 
wType.isMap())) {
                                out.nlIf(! isRoot, xIndent + 1);
                                if (o instanceof BeanMap o2)
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParserSession.java
index 0f581ed807..65180dbcc0 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParserSession.java
@@ -35,6 +35,7 @@ import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
JsonParser}.
@@ -268,6 +269,8 @@ public class JsonParserSession extends ReaderParserSession {
                        o = parseCharacter(parseString(r));
                } else if (sType.isNumber()) {
                        o = parseNumber(r, (Class<? extends 
Number>)sType.inner());
+               } else if (sType.isDateOrCalendarOrTemporal() || 
sType.isDuration()) {
+                       o = Iso8601Utils.parse(parseString(r), sType, 
getTimeZone());
                } else if (sType.isMap()) {
                        Map m = (sType.canCreateNewInstance(outer) ? 
(Map)sType.newInstance(outer) : newGenericMap(sType));
                        o = parseIntoMap2(r, m, sType.getKeyType(), 
sType.getValueType(), pMeta);
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
index 0fe6b80c6b..2dfec57ff3 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
@@ -31,6 +31,7 @@ import org.apache.juneau.commons.lang.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
JsonSerializer}.
@@ -436,6 +437,10 @@ public class JsonSerializerSession extends 
WriterSerializerSession {
                        out.append("null");
                } else if (sType.isNumber() || sType.isBoolean()) {
                        out.append(o);
+               } else if (sType.isDateOrCalendarOrTemporal()) {
+                       out.stringValue(Iso8601Utils.format(o, sType, 
getTimeZone()));
+               } else if (sType.isDuration()) {
+                       out.stringValue(o.toString());
                } else if (sType.isBean()) {
                        serializeBeanMap(out, toBeanMap(o), typeName);
                } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
index a5afe3906f..665a3c08db 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
@@ -31,6 +31,7 @@ import org.apache.juneau.commons.reflect.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
MsgPackParser}.
@@ -240,6 +241,8 @@ public class MsgPackParserSession extends 
InputStreamParserSession {
                                // Do nothing.
                        } else if (sType.isBoolean() || sType.isCharSequence() 
|| sType.isChar() || sType.isNumber() || sType.isByteArray()) {
                                o = convertToType(o, sType);
+                       } else if (sType.isDateOrCalendarOrTemporal() || 
sType.isDuration()) {
+                               o = Iso8601Utils.parse(String.valueOf(o), 
sType, getTimeZone());
                        } else if (sType.isMap()) {
                                if (dt == MAP) {
                                        Map m = 
(sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : 
newGenericMap(sType));
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerSession.java
index 47cbaed3d0..1aa5dcbb91 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerSession.java
@@ -30,6 +30,7 @@ import org.apache.juneau.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
MsgPackSerializer}.
@@ -266,6 +267,10 @@ public class MsgPackSerializerSession extends 
OutputStreamSerializerSession {
                        out.appendBoolean((Boolean)o);
                else if (sType.isNumber())
                        out.appendNumber((Number)o);
+               else if (sType.isDateOrCalendarOrTemporal())
+                       out.appendString(Iso8601Utils.format(o, sType, 
getTimeZone()));
+               else if (sType.isDuration())
+                       out.appendString(o.toString());
                else if (sType.isBean())
                        serializeBeanMap(out, toBeanMap(o), typeName);
                else if (sType.isUri() || (nn(pMeta) && pMeta.isUri()))
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
index e5d9690133..93f5e8b1ed 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
@@ -37,8 +37,8 @@ import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.swap.*;
-import org.apache.juneau.swaps.*;
 import org.apache.juneau.uon.*;
+import org.apache.juneau.utils.*;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
OpenApiParser}.
@@ -298,31 +298,10 @@ public class OpenApiParserSession extends 
UonParserSession {
                                }
                                if (f == BYTE)
                                        return toType(base64Decode(in), type);
-                               if (f == DATE) {
-                                       try {
-                                               if (type.isCalendar())
-                                                       return 
toType(TemporalCalendarSwap.IsoDate.DEFAULT.unswap(this, in, type), type);
-                                               if (type.isDate())
-                                                       return 
toType(TemporalDateSwap.IsoDate.DEFAULT.unswap(this, in, type), type);
-                                               if (type.isTemporal())
-                                                       return 
toType(TemporalSwap.IsoDate.DEFAULT.unswap(this, in, type), type);
-                                               return toType(in, type);
-                                       } catch (Exception e) {
-                                               throw new ParseException(e);
-                                       }
-                               }
-                               if (f == DATE_TIME) {
-                                       try {
-                                               if (type.isCalendar())
-                                                       return 
toType(TemporalCalendarSwap.IsoDateTime.DEFAULT.unswap(this, in, type), type);
-                                               if (type.isDate())
-                                                       return 
toType(TemporalDateSwap.IsoDateTime.DEFAULT.unswap(this, in, type), type);
-                                               if (type.isTemporal())
-                                                       return 
toType(TemporalSwap.IsoDateTime.DEFAULT.unswap(this, in, type), type);
-                                               return toType(in, type);
-                                       } catch (Exception e) {
-                                               throw new ParseException(e);
-                                       }
+                               if (f == DATE || f == DATE_TIME) {
+                                       if (type.isDateOrCalendarOrTemporal())
+                                               return 
toType(Iso8601Utils.parse(in, type, getTimeZone()), type);
+                                       return toType(in, type);
                                }
                                if (f == BINARY)
                                        return toType(fromHex(in), type);
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
index 687e3674b8..e8968985a4 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
@@ -36,8 +36,8 @@ import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.svl.*;
-import org.apache.juneau.swaps.*;
 import org.apache.juneau.uon.*;
+import org.apache.juneau.utils.*;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
OpenApiSerializer}.
@@ -342,31 +342,9 @@ public class OpenApiSerializerSession extends 
UonSerializerSession {
                                } else if (f == BINARY_SPACED) {
                                        out = toSpacedHex(toType(value, 
CM_ByteArray));
                                } else if (f == DATE) {
-                                       try {
-                                               if (value instanceof Calendar 
value2)
-                                                       out = 
TemporalCalendarSwap.IsoDate.DEFAULT.swap(this, value2);
-                                               else if (value instanceof Date 
value2)
-                                                       out = 
TemporalDateSwap.IsoDate.DEFAULT.swap(this, value2);
-                                               else if (value instanceof 
Temporal value2)
-                                                       out = 
TemporalSwap.IsoDate.DEFAULT.swap(this, value2);
-                                               else
-                                                       out = value.toString();
-                                       } catch (Exception e) {
-                                               throw new SerializeException(e);
-                                       }
+                                       out = Iso8601Utils.formatAsDate(value, 
type, getTimeZone());
                                } else if (f == DATE_TIME) {
-                                       try {
-                                               if (value instanceof Calendar 
value2)
-                                                       out = 
TemporalCalendarSwap.IsoInstant.DEFAULT.swap(this, value2);
-                                               else if (value instanceof Date 
value2)
-                                                       out = 
TemporalDateSwap.IsoInstant.DEFAULT.swap(this, value2);
-                                               else if (value instanceof 
Temporal value2)
-                                                       out = 
TemporalSwap.IsoInstant.DEFAULT.swap(this, value2);
-                                               else
-                                                       out = value.toString();
-                                       } catch (Exception e) {
-                                               throw new SerializeException(e);
-                                       }
+                                       out = 
Iso8601Utils.formatAsDateTime(value, type, getTimeZone());
                                } else if (f == HttpPartFormat.UON) {
                                        out = super.serialize(partType, schema, 
value);
                                } else {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
index b0481b5c7a..34d1c75a15 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
@@ -38,6 +38,7 @@ import org.apache.juneau.cp.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.objecttools.*;
 import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
Parser}.
@@ -826,6 +827,8 @@ public class ParserSession extends BeanSession {
                        o = parseNumber(s, (Class<? extends 
Number>)sType.inner());
                else if (sType.isBoolean())
                        o = Boolean.parseBoolean(s);
+               else if (sType.isDateOrCalendarOrTemporal() || 
sType.isDuration())
+                       o = Iso8601Utils.parse(s, sType, getTimeZone());
                else if (! (sType.isCharSequence() || sType.isObject())) {
                        if (sType.canCreateNewInstanceFromString(outer))
                                o = sType.newInstanceFromString(outer, s);
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerSession.java
index 2b1079ec20..461f4718b0 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerSession.java
@@ -37,6 +37,7 @@ import org.apache.juneau.parser.*;
 import org.apache.juneau.soap.*;
 import org.apache.juneau.svl.*;
 import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
 
 /**
  * Serializer session that lives for the duration of a single use of {@link 
Serializer}.
@@ -709,6 +710,9 @@ public class SerializerSession extends BeanTraverseSession {
                        return ((ClassInfo)o).getNameFull();
                if (o.getClass().isEnum())
                        return getClassMetaForObject(o).toString(o);
+               var cm = getClassMetaForObject(o);
+               if (cm.isDateOrCalendarOrTemporal() || cm.isDuration())
+                       return Iso8601Utils.format(o, cm, getTimeZone());
                var s = o.toString();
                if (isTrimStrings())
                        s = s.trim();
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swap/DefaultSwaps.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swap/DefaultSwaps.java
index e223c20d0b..71b5c39110 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swap/DefaultSwaps.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swap/DefaultSwaps.java
@@ -20,13 +20,10 @@ import static org.apache.juneau.commons.utils.Utils.*;
 
 import java.net.*;
 import java.time.*;
-import java.time.temporal.*;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.regex.*;
 
-import javax.xml.datatype.*;
-
 import org.apache.juneau.commons.reflect.*;
 import org.apache.juneau.swaps.*;
 
@@ -48,21 +45,8 @@ public class DefaultSwaps {
        static {
                SWAPS.put(Locale.class, new LocaleSwap());
                SWAPS.put(Class.class, new ClassSwap());
-               SWAPS.put(Calendar.class, new 
TemporalCalendarSwap.IsoOffsetDateTime());
-               SWAPS.put(Date.class, new TemporalDateSwap.IsoLocalDateTime());
-               SWAPS.put(Instant.class, new TemporalSwap.IsoInstant());
-               SWAPS.put(ZonedDateTime.class, new 
TemporalSwap.IsoOffsetDateTime());
-               SWAPS.put(LocalDate.class, new TemporalSwap.IsoLocalDate());
-               SWAPS.put(LocalDateTime.class, new 
TemporalSwap.IsoLocalDateTime());
-               SWAPS.put(LocalTime.class, new TemporalSwap.IsoLocalTime());
-               SWAPS.put(OffsetDateTime.class, new 
TemporalSwap.IsoOffsetDateTime());
-               SWAPS.put(OffsetTime.class, new TemporalSwap.IsoOffsetTime());
                SWAPS.put(StackTraceElement.class, new StackTraceElementSwap());
-               SWAPS.put(Year.class, new TemporalSwap.IsoYear());
-               SWAPS.put(YearMonth.class, new TemporalSwap.IsoYearMonth());
-               SWAPS.put(Temporal.class, new TemporalSwap.IsoInstant());
                SWAPS.put(TimeZone.class, new TimeZoneSwap());
-               SWAPS.put(XMLGregorianCalendar.class, new 
XMLGregorianCalendarSwap());
                SWAPS.put(ZoneId.class, new ZoneIdSwap());
                SWAPS.put(MatchResult.class, new MatchResultSwap());
                SWAPS.put(URL.class, new UrlSwap());
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalCalendarSwap.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalCalendarSwap.java
index 2621d29d99..4be9de1b83 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalCalendarSwap.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalCalendarSwap.java
@@ -31,6 +31,11 @@ import org.apache.juneau.swap.*;
  * <p>
  * Uses the {@link DateTimeFormatter} class for converting {@link Calendar} 
objects.
  *
+ * <p>
+ * Date/time types are now serialized as ISO 8601 strings by default without 
needing a swap.
+ * These swap classes can be used to override the default format (e.g., using 
RFC 1123 instead of ISO 8601).
+ * They can be registered globally via {@link BeanContext#swaps()} or 
per-field via the {@link Swap @Swap} annotation.
+ *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/SwapBasics";>Swap Basics</a>
  * </ul>
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalDateSwap.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalDateSwap.java
index 566aa43d55..cc53fec586 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalDateSwap.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalDateSwap.java
@@ -31,6 +31,11 @@ import org.apache.juneau.swap.*;
  * <p>
  * Uses the {@link DateTimeFormatter} class for converting {@link Date} 
objects.
  *
+ * <p>
+ * Date/time types are now serialized as ISO 8601 strings by default without 
needing a swap.
+ * These swap classes can be used to override the default format (e.g., using 
RFC 1123 instead of ISO 8601).
+ * They can be registered globally via {@link BeanContext#swaps()} or 
per-field via the {@link Swap @Swap} annotation.
+ *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/SwapBasics";>Swap Basics</a>
  * </ul>
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalSwap.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalSwap.java
index 47741dcffa..faf0988ce7 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalSwap.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/TemporalSwap.java
@@ -55,6 +55,11 @@ import org.apache.juneau.swap.*;
  *     <li class='jc'>{@link ZonedDateTime}
  * </ul>
  *
+ * <p>
+ * Date/time types are now serialized as ISO 8601 strings by default without 
needing a swap.
+ * These swap classes can be used to override the default format (e.g., using 
RFC 1123 instead of ISO 8601).
+ * They can be registered globally via {@link BeanContext#swaps()} or 
per-field via the {@link Swap @Swap} annotation.
+ *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/SwapBasics";>Swap Basics</a>
  * </ul>
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/XMLGregorianCalendarSwap.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/XMLGregorianCalendarSwap.java
index b0e7ac3bd5..79ea229b20 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/XMLGregorianCalendarSwap.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/XMLGregorianCalendarSwap.java
@@ -33,6 +33,11 @@ import org.apache.juneau.swap.*;
  * <p>
  * Strings are converted to objects using {@link 
DatatypeFactory#newXMLGregorianCalendar(String)}.
  *
+ * <p>
+ * Date/time types are now serialized as ISO 8601 strings by default without 
needing a swap.
+ * These swap classes can be used to override the default format (e.g., using 
RFC 1123 instead of ISO 8601).
+ * They can be registered globally via {@link BeanContext#swaps()} or 
per-field via the {@link Swap @Swap} annotation.
+ *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/SwapBasics";>Swap Basics</a>
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonParserSession.java
index 1da56f0ec8..39c986642c 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonParserSession.java
@@ -36,6 +36,7 @@ import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
UonParser}.
@@ -377,6 +378,8 @@ public class UonParserSession extends ReaderParserSession 
implements HttpPartPar
                        o = parseCharacter(parseString(r, isUrlParamValue));
                } else if (sType.isNumber()) {
                        o = parseNumber(r, (Class<? extends 
Number>)sType.inner());
+               } else if (sType.isDateOrCalendarOrTemporal() || 
sType.isDuration()) {
+                       o = Iso8601Utils.parse(parseString(r, isUrlParamValue), 
sType, getTimeZone());
                } else if (sType.isMap()) {
                        var m = (sType.canCreateNewInstance(outer) ? 
(Map)sType.newInstance(outer) : newGenericMap(sType));
                        o = parseIntoMap(r, m, sType.getKeyType(), 
sType.getValueType(), pMeta);
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
index fc3cd01be3..dac79b4d4f 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
@@ -33,6 +33,7 @@ import org.apache.juneau.httppart.*;
 import org.apache.juneau.reflect.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
UonSerializer}.
@@ -483,6 +484,10 @@ public class UonSerializerSession extends 
WriterSerializerSession implements Htt
                        out.appendBoolean(o);
                else if (sType.isNumber())
                        out.appendNumber(o);
+               else if (sType.isDateOrCalendarOrTemporal())
+                       out.appendObject(Iso8601Utils.format(o, sType, 
getTimeZone()), false);
+               else if (sType.isDuration())
+                       out.appendObject(o.toString(), false);
                else if (sType.isBean())
                        serializeBeanMap(out, toBeanMap(o), typeName);
                else if (sType.isUri() || (nn(pMeta) && pMeta.isUri()))
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/Iso8601Utils.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/Iso8601Utils.java
new file mode 100644
index 0000000000..61ddee7b77
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/Iso8601Utils.java
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.utils;
+
+import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
+
+import java.lang.reflect.*;
+import java.time.*;
+import java.time.format.*;
+import java.time.temporal.*;
+import java.util.*;
+
+import javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.swaps.*;
+
+/**
+ * Centralized ISO 8601 formatting and parsing utility for date/time and 
Duration types.
+ *
+ * <p>
+ * Provides the built-in serialization format for {@link 
java.time.temporal.Temporal}, {@link Calendar},
+ * {@link Date}, {@link XMLGregorianCalendar}, and {@link Duration} objects.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/Marshalling";>Marshalling</a>
+ * </ul>
+ */
+public final class Iso8601Utils {
+
+       private Iso8601Utils() {}
+
+       private static final ZoneId Z = ZoneId.of("Z");
+
+       private static final DateTimeFormatter YEAR_FORMATTER = 
DateTimeFormatter.ofPattern("uuuu");
+       private static final DateTimeFormatter YEAR_MONTH_FORMATTER = 
DateTimeFormatter.ofPattern("uuuu-MM");
+
+       private static final Map<Class<?>, DateTimeFormatter> 
DEFAULT_FORMATTERS = Map.ofEntries(
+               Map.entry(Instant.class, DateTimeFormatter.ISO_INSTANT),
+               Map.entry(ZonedDateTime.class, 
DateTimeFormatter.ISO_OFFSET_DATE_TIME),
+               Map.entry(OffsetDateTime.class, 
DateTimeFormatter.ISO_OFFSET_DATE_TIME),
+               Map.entry(LocalDate.class, DateTimeFormatter.ISO_LOCAL_DATE),
+               Map.entry(LocalDateTime.class, 
DateTimeFormatter.ISO_LOCAL_DATE_TIME),
+               Map.entry(LocalTime.class, DateTimeFormatter.ISO_LOCAL_TIME),
+               Map.entry(OffsetTime.class, DateTimeFormatter.ISO_OFFSET_TIME),
+               Map.entry(Year.class, YEAR_FORMATTER),
+               Map.entry(YearMonth.class, YEAR_MONTH_FORMATTER)
+       );
+
+       private static DatatypeFactory datatypeFactory;
+       static {
+               try {
+                       datatypeFactory = DatatypeFactory.newInstance();
+               } catch (DatatypeConfigurationException e) {
+                       throw toRex(e);
+               }
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Formatting
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       /**
+        * Formats a date/time or Duration value to its ISO 8601 string 
representation.
+        *
+        * @param value The value to format.
+        * @param type The class metadata for the value (used for selecting the 
appropriate formatter for Temporal types).
+        * @param timeZone The session time zone (used when the value lacks 
zone info).
+        * @return The ISO 8601 string representation.
+        */
+       public static String format(Object value, ClassMeta<?> type, TimeZone 
timeZone) {
+               if (value instanceof Duration d)
+                       return d.toString();
+               if (value instanceof Calendar c)
+                       return formatCalendar(c);
+               if (value instanceof Date d)
+                       return formatDate(d, timeZone);
+               if (value instanceof XMLGregorianCalendar x)
+                       return x.toXMLFormat();
+               if (value instanceof Temporal t)
+                       return formatTemporal(t, timeZone);
+               return value.toString();
+       }
+
+       /**
+        * Formats a Calendar to ISO 8601 using ISO_OFFSET_DATE_TIME.
+        */
+       private static String formatCalendar(Calendar c) {
+               ZonedDateTime zdt;
+               if (c instanceof GregorianCalendar gc)
+                       zdt = gc.toZonedDateTime();
+               else
+                       zdt = c.toInstant().atZone(c.getTimeZone().toZoneId());
+               return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zdt);
+       }
+
+       /**
+        * Formats a Date to ISO 8601 using ISO_LOCAL_DATE_TIME.
+        */
+       private static String formatDate(Date d, TimeZone tz) {
+               ZoneId zoneId = tz != null ? tz.toZoneId() : 
ZoneId.systemDefault();
+               return 
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(d.toInstant().atZone(zoneId));
+       }
+
+       /**
+        * Formats a Temporal to ISO 8601 using the appropriate default 
formatter for the concrete type.
+        */
+       private static String formatTemporal(Temporal t, TimeZone tz) {
+               ZoneId zoneId = tz != null ? tz.toZoneId() : 
ZoneId.systemDefault();
+               Class<?> tc = t.getClass();
+
+               if (tc == Instant.class)
+                       return 
DateTimeFormatter.ISO_INSTANT.format(ZonedDateTime.from(new 
DefaultingTemporalAccessor(t, Z)));
+
+               if (tc == ZonedDateTime.class || tc == OffsetDateTime.class || 
tc == OffsetTime.class)
+                       return DEFAULT_FORMATTERS.getOrDefault(tc, 
DateTimeFormatter.ISO_OFFSET_DATE_TIME).format(t);
+
+               if (tc == LocalDate.class || tc == LocalDateTime.class || tc == 
LocalTime.class
+                               || tc == Year.class || tc == YearMonth.class)
+                       return DEFAULT_FORMATTERS.getOrDefault(tc, 
DateTimeFormatter.ISO_LOCAL_DATE_TIME).format(t);
+
+               return 
DateTimeFormatter.ISO_INSTANT.format(ZonedDateTime.from(new 
DefaultingTemporalAccessor(t, zoneId)));
+       }
+
+       /**
+        * Formats a date/time value as an ISO date (date-only, for OpenAPI 
'date' format).
+        *
+        * @param value The value to format.
+        * @param type The class metadata.
+        * @param timeZone The session time zone.
+        * @return The ISO date string.
+        */
+       public static String formatAsDate(Object value, ClassMeta<?> type, 
TimeZone timeZone) {
+               ZoneId zoneId = timeZone != null ? timeZone.toZoneId() : 
ZoneId.systemDefault();
+               if (value instanceof Calendar c) {
+                       ZonedDateTime zdt = (c instanceof GregorianCalendar gc) 
? gc.toZonedDateTime() : c.toInstant().atZone(c.getTimeZone().toZoneId());
+                       return DateTimeFormatter.ISO_DATE.format(zdt);
+               }
+               if (value instanceof Date d)
+                       return 
DateTimeFormatter.ISO_DATE.format(d.toInstant().atZone(zoneId));
+               if (value instanceof Temporal t)
+                       return 
DateTimeFormatter.ISO_DATE.format(ZonedDateTime.from(new 
DefaultingTemporalAccessor(t, zoneId)));
+               return value.toString();
+       }
+
+       /**
+        * Formats a date/time value as an ISO date-time (for OpenAPI 
'date-time' format).
+        *
+        * @param value The value to format.
+        * @param type The class metadata.
+        * @param timeZone The session time zone.
+        * @return The ISO date-time string.
+        */
+       public static String formatAsDateTime(Object value, ClassMeta<?> type, 
TimeZone timeZone) {
+               ZoneId zoneId = timeZone != null ? timeZone.toZoneId() : 
ZoneId.systemDefault();
+               if (value instanceof Calendar c) {
+                       ZonedDateTime zdt = (c instanceof GregorianCalendar gc) 
? gc.toZonedDateTime() : c.toInstant().atZone(c.getTimeZone().toZoneId());
+                       return 
DateTimeFormatter.ISO_INSTANT.format(zdt.toInstant());
+               }
+               if (value instanceof Date d)
+                       return 
DateTimeFormatter.ISO_INSTANT.format(d.toInstant());
+               if (value instanceof Temporal t) {
+                       ZonedDateTime zdt = ZonedDateTime.from(new 
DefaultingTemporalAccessor(t, zoneId));
+                       return 
DateTimeFormatter.ISO_INSTANT.format(zdt.toInstant());
+               }
+               return value.toString();
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Parsing
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       /**
+        * Parses an ISO 8601 string into the specified date/time target type.
+        *
+        * @param <T> The target type.
+        * @param iso8601 The ISO 8601 string to parse.
+        * @param targetType The target class metadata.
+        * @param timeZone The session time zone (used for types that need a 
default zone).
+        * @return The parsed object.
+        */
+       @SuppressWarnings("unchecked")
+       public static <T> T parse(String iso8601, ClassMeta<T> targetType, 
TimeZone timeZone) {
+               if (iso8601 == null)
+                       return null;
+
+               Class<T> tc = targetType.inner();
+               ZoneId zoneId = timeZone != null ? timeZone.toZoneId() : 
ZoneId.systemDefault();
+
+               if (tc == Duration.class)
+                       return (T) Duration.parse(iso8601);
+
+               if (Calendar.class.isAssignableFrom(tc))
+                       return (T) parseCalendar(iso8601, zoneId);
+
+               if (tc == Date.class)
+                       return (T) parseDate(iso8601, zoneId);
+
+               if (XMLGregorianCalendar.class.isAssignableFrom(tc))
+                       return (T) 
datatypeFactory.newXMLGregorianCalendar(iso8601);
+
+               if (Temporal.class.isAssignableFrom(tc))
+                       return (T) parseTemporal(iso8601, (Class<? extends 
Temporal>) tc, zoneId);
+
+               return null;
+       }
+
+       private static Calendar parseCalendar(String iso8601, ZoneId zoneId) {
+               var formatter = selectParserFormatter(iso8601);
+               var ta = new 
DefaultingTemporalAccessor(formatter.parse(iso8601), zoneId);
+               return GregorianCalendar.from(ZonedDateTime.from(ta));
+       }
+
+       private static Date parseDate(String iso8601, ZoneId zoneId) {
+               var formatter = selectParserFormatter(iso8601);
+               var ta = new 
DefaultingTemporalAccessor(formatter.parse(iso8601), zoneId);
+               return Date.from(ZonedDateTime.from(ta).toInstant());
+       }
+
+       @SuppressWarnings("unchecked")
+       private static <T extends Temporal> T parseTemporal(String iso8601, 
Class<T> tc, ZoneId zoneId) {
+               ZoneId offset = (tc == Instant.class) ? Z : zoneId;
+               var formatter = getFormatterForType(tc);
+               var ta = new 
DefaultingTemporalAccessor(formatter.parse(iso8601), offset);
+
+               try {
+                       var parseMethod = info(tc).getPublicMethod(
+                               x -> x.isStatic()
+                                       && x.isNotDeprecated()
+                                       && x.hasName("from")
+                                       && x.hasReturnType(tc)
+                                       && 
x.hasParameterTypes(TemporalAccessor.class)
+                       ).map(MethodInfo::inner).orElse(null);
+
+                       if (parseMethod != null)
+                               return (T) parseMethod.invoke(null, ta);
+               } catch (IllegalAccessException | InvocationTargetException e) {
+                       throw toRex(e);
+               }
+
+               throw new IllegalArgumentException("Cannot parse ISO 8601 
string into type: " + tc.getName());
+       }
+
+       private static DateTimeFormatter getFormatterForType(Class<?> tc) {
+               var f = DEFAULT_FORMATTERS.get(tc);
+               return f != null ? f : DateTimeFormatter.ISO_INSTANT;
+       }
+
+       /**
+        * Auto-detects the appropriate parser formatter based on the string 
content.
+        */
+       private static DateTimeFormatter selectParserFormatter(String iso8601) {
+               boolean hasTime = iso8601.contains("T");
+               boolean hasZone = iso8601.endsWith("Z") || iso8601.contains("+")
+                       || (iso8601.length() > 10 && iso8601.lastIndexOf('-') > 
10);
+               if (hasTime && hasZone)
+                       return DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+               if (hasTime)
+                       return DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+               if (hasZone)
+                       return DateTimeFormatter.ISO_DATE;
+               return DateTimeFormatter.ISO_LOCAL_DATE;
+       }
+
+       /**
+        * Converts epoch milliseconds to the target date/time type.
+        *
+        * @param <T> The target type.
+        * @param epochMillis The epoch milliseconds value.
+        * @param targetType The target class metadata.
+        * @param timeZone The session time zone.
+        * @return The converted date/time object.
+        */
+       @SuppressWarnings("unchecked")
+       public static <T> T fromEpochMillis(long epochMillis, ClassMeta<T> 
targetType, TimeZone timeZone) {
+               Class<T> tc = targetType.inner();
+               ZoneId zoneId = timeZone != null ? timeZone.toZoneId() : 
ZoneId.systemDefault();
+               Instant instant = Instant.ofEpochMilli(epochMillis);
+
+               if (tc == Instant.class) return (T) instant;
+               if (tc == ZonedDateTime.class) return (T) 
instant.atZone(zoneId);
+               if (tc == OffsetDateTime.class) return (T) 
instant.atZone(zoneId).toOffsetDateTime();
+               if (tc == LocalDateTime.class) return (T) 
instant.atZone(zoneId).toLocalDateTime();
+               if (tc == LocalDate.class) return (T) 
instant.atZone(zoneId).toLocalDate();
+               if (tc == LocalTime.class) return (T) 
instant.atZone(zoneId).toLocalTime();
+               if (tc == OffsetTime.class) return (T) 
instant.atZone(zoneId).toOffsetDateTime().toOffsetTime();
+               if (tc == Date.class) return (T) Date.from(instant);
+               if (Calendar.class.isAssignableFrom(tc)) {
+                       var cal = 
GregorianCalendar.from(instant.atZone(zoneId));
+                       return (T) cal;
+               }
+
+               return null;
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlParserSession.java
index b48781f8de..25d1f0ada0 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlParserSession.java
@@ -38,6 +38,7 @@ import org.apache.juneau.commons.reflect.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
XmlParser}.
@@ -870,6 +871,8 @@ public class XmlParserSession extends ReaderParserSession {
                        o = parseIntoCollection(r, l, sType, pMeta);
                } else if (sType.isNumber()) {
                        o = parseNumber(getElementText(r), (Class<? extends 
Number>)sType.inner());
+               } else if (sType.isDateOrCalendarOrTemporal() || 
sType.isDuration()) {
+                       o = Iso8601Utils.parse(getElementText(r), sType, 
getTimeZone());
                } else if (nn(builder) || sType.canCreateNewBean(outer)) {
                        if (getXmlClassMeta(sType).getFormat() == COLLAPSED) {
                                var fieldName = r.getLocalName();
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
index bf132d8152..0152a2cbf1 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
@@ -35,6 +35,7 @@ import org.apache.juneau.commons.lang.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
 import org.apache.juneau.xml.annotation.*;
 
 /**
@@ -996,6 +997,10 @@ public class XmlSerializerSession extends 
WriterSerializerSession {
                                        out.text(o, preserveWhitespace);
                        } else if (sType.isNumber() || sType.isBoolean()) {
                                out.append(o);
+                       } else if (sType.isDateOrCalendarOrTemporal()) {
+                               out.text(Iso8601Utils.format(o, sType, 
getTimeZone()));
+                       } else if (sType.isDuration()) {
+                               out.text(o.toString());
                        } else if (sType.isMap() || (nn(wType) && 
wType.isMap())) {
                                if (o instanceof BeanMap o2)
                                        rc = serializeBeanMap(out, o2, 
elementNamespace, isCollapsed, isMixedOrText);
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
new file mode 100644
index 0000000000..b1e6fd7c82
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
@@ -0,0 +1,368 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.a.rttests;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.*;
+import java.util.*;
+
+import javax.xml.datatype.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.html.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.msgpack.*;
+import org.apache.juneau.uon.*;
+import org.apache.juneau.urlencoding.*;
+import org.apache.juneau.xml.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Round-trip tests for built-in date/time and Duration serialization across 
all serializer/parser combinations.
+ */
+class RoundTripDateTime_Test extends TestBase {
+
+       private static final RoundTrip_Tester[] TESTERS = {
+               tester(1, "Json - default")
+                       
.serializer(JsonSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create())
+                       .build(),
+               tester(2, "Json - lax")
+                       
.serializer(JsonSerializer.create().json5().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create())
+                       .build(),
+               tester(3, "Json - lax, readable")
+                       
.serializer(JsonSerializer.create().json5().ws().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create())
+                       .build(),
+               tester(4, "Xml - namespaces, validation, readable")
+                       
.serializer(XmlSerializer.create().ns().sq().keepNullProperties().addNamespaceUrisToRoot().useWhitespace().addBeanTypes().addRootType())
+                       .parser(XmlParser.create())
+                       .validateXmlWhitespace()
+                       .validateXml()
+                       .build(),
+               tester(5, "Xml - no namespaces, validation")
+                       
.serializer(XmlSerializer.create().sq().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(XmlParser.create())
+                       .validateXmlWhitespace()
+                       .build(),
+               tester(6, "Html - default")
+                       
.serializer(HtmlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(HtmlParser.create())
+                       .validateXmlWhitespace()
+                       .build(),
+               tester(7, "Html - readable")
+                       
.serializer(HtmlSerializer.create().sq().ws().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(HtmlParser.create())
+                       .validateXmlWhitespace()
+                       .build(),
+               tester(8, "Html - with key/value headers")
+                       
.serializer(HtmlSerializer.create().addKeyValueTableHeaders().addBeanTypes().addRootType())
+                       .parser(HtmlParser.create())
+                       .validateXmlWhitespace()
+                       .build(),
+               tester(9, "Uon - default")
+                       
.serializer(UonSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(UonParser.create())
+                       .build(),
+               tester(10, "Uon - readable")
+                       
.serializer(UonSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(UonParser.create())
+                       .build(),
+               tester(11, "Uon - encoded")
+                       
.serializer(UonSerializer.create().encoding().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(UonParser.create().decoding())
+                       .build(),
+               tester(12, "UrlEncoding - default")
+                       
.serializer(UrlEncodingSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(UrlEncodingParser.create())
+                       .build(),
+               tester(13, "UrlEncoding - readable")
+                       
.serializer(UrlEncodingSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(UrlEncodingParser.create())
+                       .build(),
+               tester(14, "UrlEncoding - expanded params")
+                       
.serializer(UrlEncodingSerializer.create().expandedParams().addBeanTypes().addRootType())
+                       .parser(UrlEncodingParser.create().expandedParams())
+                       .build(),
+               tester(15, "MsgPack")
+                       
.serializer(MsgPackSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(MsgPackParser.create())
+                       .build(),
+               tester(16, "Json schema")
+                       
.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .returnOriginalObject()
+                       .build(),
+       };
+
+       static RoundTrip_Tester[] testers() {
+               return TESTERS;
+       }
+
+       protected static RoundTrip_Tester.Builder tester(int index, String 
label) {
+               return RoundTrip_Tester.create(index, label);
+       }
+
+       
//====================================================================================================
+       // Bean with all date/time types
+       
//====================================================================================================
+
+       public static class DateTimeBean {
+               public Instant instant;
+               public ZonedDateTime zonedDateTime;
+               public LocalDate localDate;
+               public LocalDateTime localDateTime;
+               public LocalTime localTime;
+               public OffsetDateTime offsetDateTime;
+               public OffsetTime offsetTime;
+               public Year year;
+               public YearMonth yearMonth;
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a01_dateTimeBean(RoundTrip_Tester t) throws Exception {
+               var x = new DateTimeBean();
+               x.instant = Instant.parse("2012-12-21T12:34:56Z");
+               x.zonedDateTime = ZonedDateTime.parse("2012-12-21T12:34:56Z");
+               x.localDate = LocalDate.parse("2012-12-21");
+               x.localDateTime = LocalDateTime.parse("2012-12-21T12:34:56");
+               x.localTime = LocalTime.parse("12:34:56");
+               x.offsetDateTime = 
OffsetDateTime.parse("2012-12-21T12:34:56-05:00");
+               x.offsetTime = OffsetTime.parse("12:34:56-05:00");
+               x.year = Year.of(2012);
+               x.yearMonth = YearMonth.of(2012, 12);
+
+               x = t.roundTrip(x);
+
+               assertEquals(Instant.parse("2012-12-21T12:34:56Z"), x.instant);
+               
assertEquals(ZonedDateTime.parse("2012-12-21T12:34:56Z").toInstant(), 
x.zonedDateTime.toInstant());
+               assertEquals(LocalDate.parse("2012-12-21"), x.localDate);
+               assertEquals(LocalDateTime.parse("2012-12-21T12:34:56"), 
x.localDateTime);
+               assertEquals(LocalTime.parse("12:34:56"), x.localTime);
+               
assertEquals(OffsetDateTime.parse("2012-12-21T12:34:56-05:00").toInstant(), 
x.offsetDateTime.toInstant());
+               assertEquals(OffsetTime.parse("12:34:56-05:00"), x.offsetTime);
+               assertEquals(Year.of(2012), x.year);
+               assertEquals(YearMonth.of(2012, 12), x.yearMonth);
+       }
+
+       
//====================================================================================================
+       // Bean with Calendar and Date
+       
//====================================================================================================
+
+       public static class LegacyDateBean {
+               public Calendar calendar;
+               public Date date;
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a02_legacyDateBean(RoundTrip_Tester t) throws Exception {
+               var x = new LegacyDateBean();
+               x.calendar = 
GregorianCalendar.from(ZonedDateTime.parse("2012-12-21T12:34:56Z"));
+               x.date = Date.from(Instant.parse("2012-12-21T12:34:56Z"));
+
+               var millis1 = x.calendar.getTimeInMillis();
+               var millis2 = x.date.getTime();
+
+               x = t.roundTrip(x);
+
+               assertEquals(millis1, x.calendar.getTimeInMillis());
+               assertEquals(millis2, x.date.getTime());
+       }
+
+       
//====================================================================================================
+       // Bean with Duration fields
+       
//====================================================================================================
+
+       public static class DurationBean {
+               public java.time.Duration timeout;
+               public java.time.Duration interval;
+               public java.time.Duration zero;
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a03_durationBean(RoundTrip_Tester t) throws Exception {
+               var x = new DurationBean();
+               x.timeout = java.time.Duration.ofHours(1).plusMinutes(30);
+               x.interval = java.time.Duration.ofSeconds(45);
+               x.zero = java.time.Duration.ZERO;
+
+               x = t.roundTrip(x);
+
+               assertEquals(java.time.Duration.ofHours(1).plusMinutes(30), 
x.timeout);
+               assertEquals(java.time.Duration.ofSeconds(45), x.interval);
+               assertEquals(java.time.Duration.ZERO, x.zero);
+       }
+
+       
//====================================================================================================
+       // Bean with mixed date/time and Duration fields
+       
//====================================================================================================
+
+       public static class MixedBean {
+               public Instant created;
+               public LocalDate date;
+               public java.time.Duration timeout;
+               public Calendar calendar;
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a04_mixedBean(RoundTrip_Tester t) throws Exception {
+               var x = new MixedBean();
+               x.created = Instant.parse("2012-12-21T12:34:56Z");
+               x.date = LocalDate.parse("2012-12-21");
+               x.timeout = java.time.Duration.ofHours(1).plusMinutes(30);
+               x.calendar = 
GregorianCalendar.from(ZonedDateTime.parse("2012-12-21T12:34:56Z"));
+
+               var calMillis = x.calendar.getTimeInMillis();
+
+               x = t.roundTrip(x);
+
+               assertEquals(Instant.parse("2012-12-21T12:34:56Z"), x.created);
+               assertEquals(LocalDate.parse("2012-12-21"), x.date);
+               assertEquals(java.time.Duration.ofHours(1).plusMinutes(30), 
x.timeout);
+               assertEquals(calMillis, x.calendar.getTimeInMillis());
+       }
+
+       
//====================================================================================================
+       // Bean with null date/time fields
+       
//====================================================================================================
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a05_nullFields(RoundTrip_Tester t) throws Exception {
+               var x = new MixedBean();
+
+               x = t.roundTrip(x);
+
+               assertNull(x.created);
+               assertNull(x.date);
+               assertNull(x.timeout);
+               assertNull(x.calendar);
+       }
+
+       
//====================================================================================================
+       // Standalone Instant round-trip (non-URL-encoding serializers)
+       
//====================================================================================================
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a06_standaloneInstant(RoundTrip_Tester t) throws Exception {
+               if (t.isValidationOnly())
+                       return;
+
+               var s = t.getSerializer();
+               var p = t.getParser();
+
+               if (p == null)
+                       return;
+
+               var x = Instant.parse("2012-12-21T12:34:56Z");
+               try {
+                       var out = t.serialize(x, s);
+                       var x2 = p.parse(out, Instant.class);
+                       assertEquals(x, x2);
+               } catch (Exception e) {
+                       // Some serializers (UrlEncoding) may not support 
standalone non-bean values
+               }
+       }
+
+       
//====================================================================================================
+       // Standalone Duration round-trip (non-URL-encoding serializers)
+       
//====================================================================================================
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a07_standaloneDuration(RoundTrip_Tester t) throws Exception {
+               if (t.isValidationOnly())
+                       return;
+
+               var s = t.getSerializer();
+               var p = t.getParser();
+
+               if (p == null)
+                       return;
+
+               var x = java.time.Duration.ofHours(2).plusMinutes(15);
+               try {
+                       var out = t.serialize(x, s);
+                       var x2 = p.parse(out, java.time.Duration.class);
+                       assertEquals(x, x2);
+               } catch (Exception e) {
+                       // Some serializers (UrlEncoding) may not support 
standalone non-bean values
+               }
+       }
+
+       
//====================================================================================================
+       // XMLGregorianCalendar round-trip
+       
//====================================================================================================
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a08_xmlGregorianCalendar(RoundTrip_Tester t) throws Exception {
+               if (t.isValidationOnly())
+                       return;
+
+               var s = t.getSerializer();
+               var p = t.getParser();
+
+               if (p == null)
+                       return;
+
+               var gc = new GregorianCalendar();
+               
gc.setTimeInMillis(Instant.parse("2012-12-21T12:34:56Z").toEpochMilli());
+               gc.setTimeZone(TimeZone.getTimeZone("UTC"));
+               var c = 
DatatypeFactory.newInstance().newXMLGregorianCalendar(gc);
+
+               try {
+                       var out = t.serialize(c, s);
+                       var c2 = p.parse(out, XMLGregorianCalendar.class);
+                       assertEquals(c, c2);
+               } catch (Exception e) {
+                       // Some serializers (UrlEncoding) may not support 
standalone non-bean values
+               }
+       }
+
+       
//====================================================================================================
+       // Bean with negative and fractional Duration values
+       
//====================================================================================================
+
+       public static class EdgeCaseDurationBean {
+               public java.time.Duration negative;
+               public java.time.Duration fractional;
+               public java.time.Duration large;
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a09_edgeCaseDurations(RoundTrip_Tester t) throws Exception {
+               var x = new EdgeCaseDurationBean();
+               x.negative = java.time.Duration.ofHours(-6);
+               x.fractional = java.time.Duration.ofSeconds(20, 345000000);
+               x.large = java.time.Duration.ofDays(365);
+
+               x = t.roundTrip(x);
+
+               assertEquals(java.time.Duration.ofHours(-6), x.negative);
+               assertEquals(java.time.Duration.ofSeconds(20, 345000000), 
x.fractional);
+               assertEquals(java.time.Duration.ofDays(365), x.large);
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/transforms/BuiltInDateTimeSerialization_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/transforms/BuiltInDateTimeSerialization_Test.java
new file mode 100644
index 0000000000..1186d08725
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/transforms/BuiltInDateTimeSerialization_Test.java
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.transforms;
+
+import static org.apache.juneau.TestUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.*;
+import java.time.format.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests built-in first-class date/time and Duration serialization and 
round-trip parsing.
+ */
+class BuiltInDateTimeSerialization_Test extends TestBase {
+
+       @BeforeAll static void beforeClass() {
+               setTimeZone("GMT-5");
+       }
+
+       @AfterAll static void afterClass() {
+               unsetTimeZone();
+       }
+
+       private static final WriterSerializer JS = Json5Serializer.DEFAULT;
+       private static final ReaderParser JP = Json5Parser.DEFAULT;
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Instant
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void a01_json_instant() throws Exception {
+               var i = Instant.parse("2012-12-21T12:34:56Z");
+               var json = JS.serialize(i);
+               assertEquals("'2012-12-21T12:34:56Z'", json);
+               var i2 = JP.parse(json, Instant.class);
+               assertEquals(i, i2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // ZonedDateTime
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void b01_json_zonedDateTime() throws Exception {
+               var zdt = ZonedDateTime.parse("2012-12-21T12:34:56Z");
+               var json = JS.serialize(zdt);
+               assertEquals("'2012-12-21T12:34:56Z'", json);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // LocalDate
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void c01_json_localDate() throws Exception {
+               var ld = LocalDate.parse("2012-12-21");
+               var json = JS.serialize(ld);
+               assertEquals("'2012-12-21'", json);
+               var ld2 = JP.parse(json, LocalDate.class);
+               assertEquals(ld, ld2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // LocalDateTime
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void d01_json_localDateTime() throws Exception {
+               var ldt = LocalDateTime.parse("2012-12-21T12:34:56");
+               var json = JS.serialize(ldt);
+               assertEquals("'2012-12-21T12:34:56'", json);
+               var ldt2 = JP.parse(json, LocalDateTime.class);
+               assertEquals(ldt, ldt2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // LocalTime
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void e01_json_localTime() throws Exception {
+               var lt = LocalTime.parse("12:34:56");
+               var json = JS.serialize(lt);
+               assertEquals("'12:34:56'", json);
+               var lt2 = JP.parse(json, LocalTime.class);
+               assertEquals(lt, lt2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // OffsetDateTime
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void f01_json_offsetDateTime() throws Exception {
+               var odt = OffsetDateTime.parse("2012-12-21T12:34:56-05:00");
+               var json = JS.serialize(odt);
+               assertEquals("'2012-12-21T12:34:56-05:00'", json);
+               var odt2 = JP.parse(json, OffsetDateTime.class);
+               assertEquals(odt, odt2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // OffsetTime
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void g01_json_offsetTime() throws Exception {
+               var ot = OffsetTime.parse("12:34:56-05:00");
+               var json = JS.serialize(ot);
+               assertEquals("'12:34:56-05:00'", json);
+               var ot2 = JP.parse(json, OffsetTime.class);
+               assertEquals(ot, ot2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Year
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void h01_json_year() throws Exception {
+               var y = Year.parse("2012");
+               var json = JS.serialize(y);
+               assertEquals("'2012'", json);
+               var y2 = JP.parse(json, Year.class);
+               assertEquals(y, y2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // YearMonth
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void i01_json_yearMonth() throws Exception {
+               var ym = YearMonth.parse("2012-12");
+               var json = JS.serialize(ym);
+               assertEquals("'2012-12'", json);
+               var ym2 = JP.parse(json, YearMonth.class);
+               assertEquals(ym, ym2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Calendar
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void j01_json_calendar() throws Exception {
+               var c = 
GregorianCalendar.from(ZonedDateTime.parse("2012-12-21T12:34:56Z"));
+               var json = JS.serialize(c);
+               assertEquals("'2012-12-21T12:34:56Z'", json);
+               var c2 = JP.parse(json, Calendar.class);
+               assertEquals(c.getTimeInMillis(), c2.getTimeInMillis());
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Date
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void k01_json_date() throws Exception {
+               var d = 
Date.from(Instant.from(DateTimeFormatter.ISO_INSTANT.parse("2012-12-21T12:34:56Z")));
+               var json = JS.serialize(d);
+               assertEquals("'2012-12-21T07:34:56'", json);
+               var d2 = JP.parse(json, Date.class);
+               assertNotNull(d2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Duration
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void l01_json_duration_basic() throws Exception {
+               var d = Duration.ofHours(1).plusMinutes(30);
+               var json = JS.serialize(d);
+               assertEquals("'PT1H30M'", json);
+               var d2 = JP.parse(json, Duration.class);
+               assertEquals(d, d2);
+       }
+
+       @Test void l02_json_duration_hours() throws Exception {
+               var d = Duration.ofHours(48);
+               var json = JS.serialize(d);
+               assertEquals("'PT48H'", json);
+               var d2 = JP.parse(json, Duration.class);
+               assertEquals(d, d2);
+       }
+
+       @Test void l03_json_duration_seconds() throws Exception {
+               var d = Duration.ofSeconds(45);
+               var json = JS.serialize(d);
+               assertEquals("'PT45S'", json);
+               var d2 = JP.parse(json, Duration.class);
+               assertEquals(d, d2);
+       }
+
+       @Test void l04_json_duration_zero() throws Exception {
+               var d = Duration.ZERO;
+               var json = JS.serialize(d);
+               assertEquals("'PT0S'", json);
+               var d2 = JP.parse(json, Duration.class);
+               assertEquals(d, d2);
+       }
+
+       @Test void l05_json_duration_negative() throws Exception {
+               var d = Duration.ofHours(-6);
+               var json = JS.serialize(d);
+               assertEquals("'PT-6H'", json);
+               var d2 = JP.parse(json, Duration.class);
+               assertEquals(d, d2);
+       }
+
+       @Test void l06_json_duration_fractionalSeconds() throws Exception {
+               var d = Duration.ofSeconds(20, 345000000);
+               var json = JS.serialize(d);
+               assertEquals("'PT20.345S'", json);
+               var d2 = JP.parse(json, Duration.class);
+               assertEquals(d, d2);
+       }
+
+       @Test void l07_json_duration_complex() throws Exception {
+               var d = Duration.ofHours(26).plusMinutes(3);
+               var json = JS.serialize(d);
+               assertEquals("'PT26H3M'", json);
+               var d2 = JP.parse(json, Duration.class);
+               assertEquals(d, d2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Bean with Duration field
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       public static class DurationBean {
+               public Duration timeout;
+               public Duration interval;
+       }
+
+       @Test void m01_json_durationBean() throws Exception {
+               var bean = new DurationBean();
+               bean.timeout = Duration.ofHours(1).plusMinutes(30);
+               bean.interval = Duration.ofSeconds(45);
+
+               var json = JS.serialize(bean);
+               assertTrue(json.contains("'PT1H30M'"));
+               assertTrue(json.contains("'PT45S'"));
+
+               var bean2 = JP.parse(json, DurationBean.class);
+               assertEquals(bean.timeout, bean2.timeout);
+               assertEquals(bean.interval, bean2.interval);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Null handling
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void n01_json_nullDuration() throws Exception {
+               var bean = new DurationBean();
+               bean.timeout = null;
+               bean.interval = null;
+
+               var json = JS.serialize(bean);
+               var bean2 = JP.parse(json, DurationBean.class);
+               assertNull(bean2.timeout);
+               assertNull(bean2.interval);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Bean with mixed date/time and Duration fields
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       public static class MixedBean {
+               public Instant created;
+               public LocalDate date;
+               public Duration timeout;
+       }
+
+       @Test void o01_json_mixedBean() throws Exception {
+               var bean = new MixedBean();
+               bean.created = Instant.parse("2012-12-21T12:34:56Z");
+               bean.date = LocalDate.parse("2012-12-21");
+               bean.timeout = Duration.ofHours(1).plusMinutes(30);
+
+               var json = JS.serialize(bean);
+               assertTrue(json.contains("'2012-12-21T12:34:56Z'"));
+               assertTrue(json.contains("'2012-12-21'"));
+               assertTrue(json.contains("'PT1H30M'"));
+
+               var bean2 = JP.parse(json, MixedBean.class);
+               assertEquals(bean.created, bean2.created);
+               assertEquals(bean.date, bean2.date);
+               assertEquals(bean.timeout, bean2.timeout);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Collections of dates
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void p01_json_listOfLocalDates() throws Exception {
+               var dates = java.util.List.of(LocalDate.parse("2012-12-21"), 
LocalDate.parse("2013-01-15"));
+               var json = JS.serialize(dates);
+               assertTrue(json.contains("'2012-12-21'"));
+               assertTrue(json.contains("'2013-01-15'"));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/transforms/DefaultSwaps_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/transforms/DefaultSwaps_Test.java
index 1c8fd0da76..66f24fff91 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/transforms/DefaultSwaps_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/transforms/DefaultSwaps_Test.java
@@ -677,4 +677,34 @@ class DefaultSwaps_Test extends TestBase {
        @Test void i03_ZoneId_overrideAnnotation() throws Exception {
                test1("{f1:'Z',f2:'FOO'}", new IBean());
        }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       //      Duration - built-in first-class support
+       
//------------------------------------------------------------------------------------------------------------------
+       private static final java.time.Duration J = 
java.time.Duration.ofHours(1).plusMinutes(30);
+
+       public static class JSwap extends StringSwap<java.time.Duration> {
+               @Override /* ObjectSwap */
+               public String swap(BeanSession session, java.time.Duration o) 
throws Exception {
+                       return "FOO";
+               }
+       }
+
+       public static class JBean {
+               public java.time.Duration f1 = J;
+               @Swap(JSwap.class)
+               public java.time.Duration f2 = J;
+       }
+
+       @Test void j01_Duration() throws Exception {
+               test1("'PT1H30M'", J);
+       }
+
+       @Test void j02_Duration_overrideSwap() throws Exception {
+               test3("'FOO'", J, JSwap.class);
+       }
+
+       @Test void j03_Duration_overrideAnnotation() throws Exception {
+               test1("{f1:'PT1H30M',f2:'FOO'}", new JBean());
+       }
 }
\ No newline at end of file

Reply via email to