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 f3626c1a9b JCS support
f3626c1a9b is described below

commit f3626c1a9b2147c6ae323929e6049529ac0da405
Author: James Bognar <[email protected]>
AuthorDate: Mon Mar 9 08:03:30 2026 -0400

    JCS support
---
 .../src/main/java/org/apache/juneau/BeanMap.java   |   3 +
 .../src/main/java/org/apache/juneau/Context.java   |   1 +
 .../java/org/apache/juneau/json/JcsSerializer.java | 184 ++++++
 .../apache/juneau/json/JcsSerializerSession.java   | 310 ++++++++++
 .../java/org/apache/juneau/json/JcsWriter.java     | 119 ++++
 .../java/org/apache/juneau/json/JsonParser.java    |   4 +-
 .../apache/juneau/json/JsonSerializerSession.java  |   6 +-
 .../apache/juneau/json/annotation/JcsConfig.java   |  53 ++
 .../json/annotation/JcsConfigAnnotation.java       |  54 ++
 .../java/org/apache/juneau/json/package-info.java  |  11 +-
 .../java/org/apache/juneau/marshaller/Jcs.java     | 120 ++++
 .../org/apache/juneau/rest/client/RestClient.java  |  18 +
 .../org/apache/juneau/rest/client/RestRequest.java |  17 +
 .../juneau/rest/config/BasicUniversalConfig.java   |   1 +
 .../a/rttests/RoundTripAddClassAttrs_Test.java     |   5 +
 .../juneau/a/rttests/RoundTripBeanMaps_Test.java   |   5 +
 .../a/rttests/RoundTripBeansWithBuilders_Test.java |   5 +
 .../juneau/a/rttests/RoundTripDateTime_Test.java   |   5 +
 .../a/rttests/RoundTripLargeObjects_Test.java      |   5 +
 .../juneau/a/rttests/RoundTripMaps_Test.java       |   5 +
 .../juneau/a/rttests/RoundTripTest_Base.java       |   5 +
 .../a/rttests/RoundTripTransformBeans_Test.java    |   6 +
 .../org/apache/juneau/json/JcsCanonical_Test.java  | 131 ++++
 .../org/apache/juneau/json/JcsEdgeCases_Test.java  |  99 +++
 .../org/apache/juneau/json/JcsNumbers_Test.java    | 134 +++++
 .../org/apache/juneau/json/JcsSorting_Test.java    | 116 ++++
 .../org/apache/juneau/json/JcsStrings_Test.java    | 100 ++++
 .../org/apache/juneau/marshaller/Jcs_Test.java     |  55 ++
 todo/3_jcs_implementation.md                       | 666 ---------------------
 29 files changed, 1571 insertions(+), 672 deletions(-)

diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
index 0ceaf8c5c8..3c5e58d625 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
@@ -67,6 +67,9 @@ import org.apache.juneau.swap.*;
  *
  * @param <T> Specifies the type of object that this map encapsulates.
  */
+@SuppressWarnings({
+       "java:S3776" // Cognitive complexity acceptable for bean property 
iteration and filtering
+})
 public class BeanMap<T> extends AbstractMap<String,Object> implements 
Delegate<T> {
 
        /**
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
index 65c681089f..ec7a9dd7cd 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
@@ -409,6 +409,7 @@ public abstract class Context {
                 *      <li class ='ja'>{@link JsonConfig}
                 *      <li class ='ja'>{@link JsonlConfig}
                 *      <li class ='ja'>{@link JsonSchemaConfig}
+                *      <li class ='ja'>{@link 
org.apache.juneau.json.annotation.JcsConfig}
                 *      <li class ='ja'>{@link MarkdownConfig}
                 *      <li class ='ja'>{@link MsgPackConfig}
                 *      <li class ='ja'>{@link OpenApiConfig}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JcsSerializer.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JcsSerializer.java
new file mode 100644
index 0000000000..aae0f00fd8
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JcsSerializer.java
@@ -0,0 +1,184 @@
+/*
+ * 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.json;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import org.apache.juneau.serializer.*;
+
+/**
+ * Serializes POJO models to canonical JSON per RFC 8785 (JCS).
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ *     Produces media type: <bc>application/jcs+json</bc>
+ * </p>
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ *     JCS (JSON Canonicalization Scheme) produces a deterministic, 
byte-for-byte canonical
+ *     representation of JSON, enabling reliable cryptographic operations such 
as hashing and
+ *     digital signing. All canonicalization rules are defined in
+ *     <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 
8785</a>.
+ * </p>
+ * <ul class='spaced-list'>
+ *     <li>No whitespace between tokens.
+ *     <li>Object properties sorted by UTF-16 code unit order, applied 
recursively to all nested objects.
+ *     <li>Numbers serialized using ECMAScript-compatible rules: shortest 
round-trip representation,
+ *         no trailing zeros, lowercase <code>e</code>, positive exponent sign 
included (e.g. <code>1e+30</code>).
+ *     <li>Negative zero serialized as <code>0</code>.
+ *     <li>Non-ASCII characters emitted as literal UTF-8 bytes (no unnecessary 
unicode escape sequences).
+ *     <li>Array element order is preserved (not sorted).
+ * </ul>
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     <jc>// Create a bean.</jc>
+ *     MyBean <jv>bean</jv> = <jk>new</jk> 
MyBean().name(<js>"Alice"</js>).age(30);
+ *
+ *     <jc>// Serialize to canonical JSON.</jc>
+ *     String <jv>json</jv> = 
JcsSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>bean</jv>);
+ *
+ *     <jc>// Or use the Jcs marshaller convenience method.</jc>
+ *     String <jv>json</jv> = Jcs.<jsm>of</jsm>(<jv>bean</jv>);
+ * </p>
+ *
+ * <h5 class='figure'>Example output (bean with name/age):</h5>
+ * <p class='bjson'>
+ *     {<jok>"age"</jok>:<jov>30</jov>,<jok>"name"</jok>:<jov>"Alice"</jov>}
+ * </p>
+ *
+ * <h5 class='figure'>Example output with nested object:</h5>
+ * <p class='bjson'>
+ *     
{<jok>"address"</jok>:{<jok>"city"</jok>:<jov>"Denver"</jov>,<jok>"zip"</jok>:<jov>"80201"</jov>},<jok>"name"</jok>:<jov>"Alice"</jov>}
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ *     <li class='note'>This class is thread safe and reusable.
+ *     <li class='note'>
+ *             JCS output is valid JSON and can be parsed using the standard 
{@link JsonParser}.
+ *             The {@link Jcs} marshaller pairs this serializer with {@link 
JsonParser} for full round-trip support.
+ *     <li class='note'>
+ *             {@link java.math.BigDecimal} and {@link java.math.BigInteger} 
values beyond IEEE 754 double
+ *             precision range will lose precision or throw a {@link 
SerializeException}.
+ *             This is a spec-level constraint defined by RFC 8785.
+ *     <li class='note'>
+ *             {@link Double#NaN}, {@link Double#POSITIVE_INFINITY}, and 
{@link Double#NEGATIVE_INFINITY}
+ *             are not permitted and will throw a {@link SerializeException}.
+ *     <li class='note'>
+ *             Strings containing lone UTF-16 surrogate code units will throw 
{@link SerializeException}.
+ *     <li class='note'>
+ *             Uses <code>application/jcs+json</code> to avoid 
content-negotiation collision with
+ *             {@link JsonSerializer} (<code>application/json</code>). Request 
explicitly via
+ *             <c>Accept: application/jcs+json</c> for canonical output.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 8785 — JSON Canonicalization 
Scheme</a>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/Jcs";>JCS topic</a>
+ *     <li class='jc'>{@link Jcs}
+ *     <li class='jc'>{@link JsonParser}
+ * </ul>
+ */
+@SuppressWarnings({
+       "java:S110", // Inheritance depth acceptable for this class hierarchy
+       "java:S115", // Constants use UPPER_snakeCase naming convention
+})
+public class JcsSerializer extends JsonSerializer {
+
+       private static final String ARG_copyFrom = "copyFrom";
+
+       /**
+        * Builder class.
+        */
+       public static class Builder extends JsonSerializer.Builder {
+
+               /**
+                * Constructor, default settings for JCS canonical output.
+                */
+               protected Builder() {
+                       super();
+                       produces("application/jcs+json");
+                       accept("application/jcs+json");
+                       sortProperties();
+                       quoteChar('"');
+                       simpleAttrs(false);
+                       useWhitespace(false);
+               }
+
+               /**
+                * Copy constructor.
+                *
+                * @param copyFrom The builder to copy from.
+                *      <br>Cannot be <jk>null</jk>.
+                */
+               protected Builder(Builder copyFrom) {
+                       super(assertArgNotNull(ARG_copyFrom, copyFrom));
+               }
+
+               /**
+                * Copy constructor.
+                *
+                * @param copyFrom The serializer to copy from.
+                *      <br>Cannot be <jk>null</jk>.
+                */
+               protected Builder(JcsSerializer copyFrom) {
+                       super(assertArgNotNull(ARG_copyFrom, copyFrom));
+               }
+
+               @Override /* Overridden from Context.Builder */
+               public Builder copy() {
+                       return new Builder(this);
+               }
+
+               @Override /* Overridden from Context.Builder */
+               public JcsSerializer build() {
+                       return new JcsSerializer(this);
+               }
+       }
+
+       /** Default serializer. */
+       public static final JcsSerializer DEFAULT = new JcsSerializer(create());
+
+       /**
+        * Creates a new builder for this object.
+        *
+        * @return A new builder.
+        */
+       public static Builder create() {
+               return new Builder();
+       }
+
+       /**
+        * Constructor.
+        *
+        * @param builder The builder for this object.
+        */
+       public JcsSerializer(Builder builder) {
+               super(builder);
+       }
+
+       @Override /* Overridden from Context */
+       public Builder copy() {
+               return new Builder(this);
+       }
+
+       @Override /* Overridden from JsonSerializer */
+       public JcsSerializerSession.Builder createSession() {
+               return JcsSerializerSession.create(this);
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JcsSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JcsSerializerSession.java
new file mode 100644
index 0000000000..ac52049b1e
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JcsSerializerSession.java
@@ -0,0 +1,310 @@
+/*
+ * 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.json;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.math.*;
+import java.util.*;
+import java.util.function.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.lang.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Session object for {@link JcsSerializer} that produces RFC 8785 canonical 
JSON.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 8785 — JSON Canonicalization 
Scheme</a>
+ * </ul>
+ */
+@SuppressWarnings({
+       "resource", // Resource management handled externally
+       "java:S110", // Inheritance depth acceptable for this class hierarchy
+       "java:S115", // Constants use UPPER_snakeCase naming convention
+       "java:S3776" // Cognitive complexity acceptable for ECMAScript number 
formatting and map serialization
+})
+public class JcsSerializerSession extends JsonSerializerSession {
+
+       // Argument name constants for assertArgNotNull
+       private static final String ARG_ctx = "ctx";
+
+       /**
+        * Builder class.
+        */
+       public static class Builder extends JsonSerializerSession.Builder {
+
+               /**
+                * Constructor
+                *
+                * @param ctx The context creating this session.
+                *      <br>Cannot be <jk>null</jk>.
+                */
+               protected Builder(JcsSerializer ctx) {
+                       super(assertArgNotNull(ARG_ctx, ctx));
+               }
+
+               @Override
+               public JcsSerializerSession build() {
+                       return new JcsSerializerSession(this);
+               }
+       }
+
+       /**
+        * Creates a new builder for this object.
+        *
+        * @param ctx The context creating this session.
+        *      <br>Cannot be <jk>null</jk>.
+        * @return A new builder.
+        */
+       public static Builder create(JcsSerializer ctx) {
+               return new Builder(assertArgNotNull(ARG_ctx, ctx));
+       }
+
+       /**
+        * Constructor.
+        *
+        * @param builder The builder for this object.
+        */
+       protected JcsSerializerSession(Builder builder) {
+               super(builder);
+       }
+
+       /**
+        * Converts a number to ECMAScript-compatible JSON string per RFC 8785.
+        *
+        * @param n The number.
+        * @return The serialized string.
+        * @throws SerializeException If the number is NaN or Infinity.
+        */
+       public static String toEcmaNumber(Number n) {
+               if (n instanceof Double d) {
+                       if (Double.isNaN(d))
+                               throw new SerializeException("NaN is not 
permitted in JCS (RFC 8785)");
+                       if (Double.isInfinite(d))
+                               throw new SerializeException("Infinity is not 
permitted in JCS (RFC 8785)");
+                       if (d == -0.0)
+                               return "0";
+                       return formatDouble(d);
+               }
+               if (n instanceof Float f) {
+                       if (Float.isNaN(f))
+                               throw new SerializeException("NaN is not 
permitted in JCS (RFC 8785)");
+                       if (Float.isInfinite(f))
+                               throw new SerializeException("Infinity is not 
permitted in JCS (RFC 8785)");
+                       if (f == -0.0f)
+                               return "0";
+                       return formatDouble(f.doubleValue());
+               }
+               if (n instanceof BigDecimal bd)
+                       return formatBigDecimal(bd);
+               if (n instanceof BigInteger bi)
+                       return formatBigInteger(bi);
+               if (n instanceof Long || n instanceof Integer || n instanceof 
Short || n instanceof Byte) {
+                       var l = n.longValue();
+                       if (l == 0)
+                               return "0";
+                       return Long.toString(l);
+               }
+               return formatDouble(n.doubleValue());
+       }
+
+       private static final double ECMA_FIXED_LOW = 1e-6;
+       private static final double ECMA_FIXED_HIGH = 1e21;
+
+       private static String formatDouble(double d) {
+               if (d == 0.0)
+                       return "0";
+               var abs = Math.abs(d);
+               // ECMAScript uses fixed notation for |n| in [1e-6, 1e21), 
scientific otherwise
+               if (abs >= ECMA_FIXED_LOW && abs < ECMA_FIXED_HIGH) {
+                       var s = BigDecimal.valueOf(d).toPlainString();
+                       s = stripTrailingZeros(s);
+                       return s;
+               }
+               return formatDoubleScientific(d);
+       }
+
+       private static String stripTrailingZeros(String s) {
+               if (s.contains(".")) {
+                       s = s.replaceAll("0+$", "");
+                       if (s.endsWith("."))
+                               s = s.substring(0, s.length() - 1);
+               }
+               return s;
+       }
+
+       private static String formatDoubleScientific(double d) {
+               var s = Double.toString(d);
+               var idxE = s.indexOf('E');
+               if (idxE < 0)
+                       idxE = s.indexOf('e');
+               if (idxE >= 0) {
+                       var mantissa = s.substring(0, idxE);
+                       var exp = s.substring(idxE + 1);
+                       var expNum = Integer.parseInt(exp);
+                       // ECMAScript: shorten mantissa (1.0 -> 1, 2.5 -> 2.5)
+                       mantissa = stripTrailingZeros(mantissa);
+                       if (mantissa.equals("1.0") || mantissa.equals("1"))
+                               mantissa = "1";
+                       else if (mantissa.endsWith(".0"))
+                               mantissa = mantissa.substring(0, 
mantissa.length() - 2);
+                       s = mantissa + 'e' + (expNum >= 0 ? "+" + expNum : 
String.valueOf(expNum));
+               }
+               return s;
+       }
+
+       private static String formatBigDecimal(BigDecimal bd) {
+               try {
+                       var d = bd.doubleValue();
+                       if (Double.isNaN(d) || Double.isInfinite(d))
+                               throw new SerializeException("BigDecimal value 
exceeds IEEE 754 double range");
+                       return formatDouble(d);
+               } catch (@SuppressWarnings("unused") NumberFormatException e) {
+                       throw new SerializeException("BigDecimal value exceeds 
IEEE 754 double range");
+               }
+       }
+
+       private static String formatBigInteger(BigInteger bi) {
+               try {
+                       var l = bi.longValueExact();
+                       return Long.toString(l);
+               } catch (@SuppressWarnings("unused") ArithmeticException e) {
+                       throw new SerializeException("BigInteger value exceeds 
IEEE 754 safe integer range");
+               }
+       }
+
+       /**
+        * JCS key comparison: UTF-16 code unit ordering per RFC 8785.
+        *
+        * <p>
+        * Null keys (e.g., from {@code HashMap} with null key) sort before 
non-null.
+        *
+        * @param a First string.
+        * @param b Second string.
+        * @return Comparison result (negative, zero, or positive).
+        */
+       public static int jcsCompare(String a, String b) {
+               if (a == null)
+                       return b == null ? 0 : -1;
+               if (b == null)
+                       return 1;
+               var len = Math.min(a.length(), b.length());
+               for (var i = 0; i < len; i++) {
+                       var diff = Character.compare(a.charAt(i), b.charAt(i));
+                       if (diff != 0)
+                               return diff;
+               }
+               return Integer.compare(a.length(), b.length());
+       }
+
+       @Override /* Overridden from JsonSerializerSession */
+       protected JsonWriter getJsonWriter(SerializerPipe out) {
+               var output = out.getRawOutput();
+               if (output instanceof JcsWriter w)
+                       return w;
+               var w = new JcsWriter(out.getWriter(), isUseWhitespace(), 
getMaxIndent(), isEscapeSolidus(), getQuoteChar(), isSimpleAttrs(), 
isTrimStrings(), getUriResolver());
+               out.setWriter(w);
+               return w;
+       }
+
+       @SuppressWarnings({
+               "rawtypes", // Raw types necessary for generic collection/map 
serialization
+               "unchecked", // Type erasure requires unchecked casts in 
collection/map serialization
+       })
+       @Override /* Overridden from JsonSerializerSession - package 
visibility, override by making accessible */
+       protected SerializerWriter serializeMap(JsonWriter out, Map m, 
ClassMeta<?> type) throws SerializeException {
+               var keyType = type.getKeyType();
+               var valueType = type.getValueType();
+
+               var entries = new ArrayList<Map.Entry<?,?>>(m.entrySet());
+               entries.sort((a, b) -> jcsCompare(toString(a.getKey()), 
toString(b.getKey())));
+
+               var i = indent;
+               out.w('{');
+
+               var addComma = Flag.create();
+               for (Map.Entry<?,?> x : entries) {
+                       Object value = x.getValue();
+                       Object key = generalize(x.getKey(), keyType);
+                       addComma.ifSet(() -> out.w(',').smi(i)).set();
+                       out.cr(i).attr(toString(key)).w(':').s(i);
+                       serializeAnything(out, value, valueType, (key == null ? 
null : toString(key)), null);
+               }
+
+               out.cre(i - 1).w('}');
+               return out;
+       }
+
+       private static class BeanProp implements Map.Entry<String, Object> {
+               final BeanPropertyMeta pMeta;
+               final String key;
+               final Object value;
+
+               BeanProp(BeanPropertyMeta pMeta, String key, Object value) {
+                       this.pMeta = pMeta;
+                       this.key = key;
+                       this.value = value;
+               }
+
+               @Override
+               public String getKey() { return key; }
+
+               @Override
+               public Object getValue() { return value; }
+
+               @Override
+               public Object setValue(Object val) { throw new 
UnsupportedOperationException(); }
+       }
+
+       @Override /* Overridden from JsonSerializerSession */
+       protected SerializerWriter serializeBeanMap(JsonWriter out, BeanMap<?> 
m, String typeName) throws SerializeException {
+               var entries = new ArrayList<BeanProp>();
+               Predicate<Object> checkNull = x -> isKeepNullProperties() || 
nn(x);
+
+               m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
+                       var cMeta = pMeta.getClassMeta();
+                       if (nn(thrown))
+                               onBeanGetterException(pMeta, thrown);
+                       if (canIgnoreValue(cMeta, key, value))
+                               return;
+                       entries.add(new BeanProp(pMeta, key, value));
+               });
+
+               // Add _type property if present, then sort all by key using 
JCS UTF-16 comparison
+               if (nn(typeName)) {
+                       var pm = m.getMeta().getTypeProperty();
+                       entries.add(new BeanProp(pm, pm.getName(), typeName));
+               }
+               entries.sort((a, b) -> jcsCompare(a.getKey(), b.getKey()));
+
+               var i = indent;
+               out.w('{');
+
+               var addComma = Flag.create();
+               for (var entry : entries) {
+                       addComma.ifSet(() -> out.append(',').smi(i)).set();
+                       out.cr(i).attr(entry.getKey()).w(':').s(i);
+                       serializeAnything(out, entry.getValue(), 
entry.pMeta.getClassMeta(), entry.getKey(), entry.pMeta);
+               }
+
+               out.cre(i - 1).w('}');
+               return out;
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JcsWriter.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JcsWriter.java
new file mode 100644
index 0000000000..b0f27c025f
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JcsWriter.java
@@ -0,0 +1,119 @@
+/*
+ * 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.json;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+
+import static org.apache.juneau.commons.utils.ThrowableUtils.rex;
+
+/**
+ * JSON writer for JCS (RFC 8785) canonical output.
+ *
+ * <p>
+ * Extends {@link JsonWriter} with ECMAScript-compatible number serialization 
and JCS string
+ * escaping (control characters, lone surrogate validation).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 8785 — JSON Canonicalization 
Scheme</a>
+ * </ul>
+ */
+@SuppressWarnings({
+       "resource" // Writer resource managed by calling code
+})
+public class JcsWriter extends JsonWriter {
+
+       /**
+        * Constructor.
+        *
+        * @param out The writer being wrapped.
+        * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used 
in output.
+        * @param maxIndent The maximum indentation level.
+        * @param escapeSolidus If <jk>true</jk>, forward slashes should be 
escaped in the output.
+        * @param quoteChar The quote character to use (always <js>'"'</js> for 
JCS).
+        * @param simpleAttrs If <jk>true</jk>, JSON attributes will only be 
quoted when necessary.
+        * @param trimStrings If <jk>true</jk>, strings will be trimmed before 
being serialized.
+        * @param uriResolver The URI resolver for resolving URIs to absolute 
or root-relative form.
+        */
+       @SuppressWarnings({
+               "java:S107" // Constructor requires 8 parameters for JSON 
writer configuration
+       })
+       protected JcsWriter(Writer out, boolean useWhitespace, int maxIndent, 
boolean escapeSolidus, char quoteChar, boolean simpleAttrs, boolean 
trimStrings, UriResolver uriResolver) {
+               super(out, useWhitespace, maxIndent, escapeSolidus, quoteChar, 
simpleAttrs, trimStrings, uriResolver);
+       }
+
+       @Override /* Overridden from SerializerWriter */
+       public JcsWriter append(Object value) {
+               if (value == null) {
+                       w("null");
+                       return this;
+               }
+               if (value instanceof Number n)
+                       w(JcsSerializerSession.toEcmaNumber(n));
+               else
+                       w(value.toString());
+               return this;
+       }
+
+       @Override /* Overridden from JsonWriter */
+       public JcsWriter stringValue(String s) {
+               if (s == null)
+                       return this;
+               validateNoLoneSurrogates(s);
+               q();
+               for (var i = 0; i < s.length(); i++) {
+                       var c = s.charAt(i);
+                       if (c == '"')
+                               w('\\').w('"');
+                       else if (c == '\\')
+                               w('\\').w('\\');
+                       else if (c == '\b')
+                               w('\\').w('b');
+                       else if (c == '\t')
+                               w('\\').w('t');
+                       else if (c == '\n')
+                               w('\\').w('n');
+                       else if (c == '\f')
+                               w('\\').w('f');
+                       else if (c == '\r')
+                               w('\\').w('r');
+                       else if (c >= 0 && c <= 0x1F)
+                               w(String.format("\\u%04x", (int) c));
+                       else
+                               w(c);
+               }
+               q();
+               return this;
+       }
+
+       @SuppressWarnings({
+               "java:S127" // Loop counter i++ needed to skip low surrogate 
after high surrogate pair
+       })
+       private static void validateNoLoneSurrogates(String s) {
+               for (var i = 0; i < s.length(); i++) {
+                       var c = s.charAt(i);
+                       if (Character.isHighSurrogate(c)) {
+                               if (i + 1 >= s.length() || 
!Character.isLowSurrogate(s.charAt(i + 1)))
+                                       throw rex("Lone high surrogate at index 
{0} in string", i);
+                               i++;
+                       } else if (Character.isLowSurrogate(c)) {
+                               throw rex("Lone low surrogate at index {0} in 
string", i);
+                       }
+               }
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParser.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParser.java
index 79f8f9949d..4d16b35f01 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParser.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/JsonParser.java
@@ -36,7 +36,7 @@ import org.apache.juneau.parser.*;
  *
  * <h5 class='topic'>Media types</h5>
  * <p>
- * Handles <c>Content-Type</c> types:  <bc>application/json, text/json</bc>
+ * Handles <c>Content-Type</c> types:  <bc>application/json, text/json, 
application/jcs+json</bc>
  *
  * <h5 class='topic'>Description</h5>
  * <p>
@@ -155,7 +155,7 @@ public class JsonParser extends ReaderParser implements 
JsonMetaProvider {
                 * Constructor, default settings.
                 */
                protected Builder() {
-                       consumes("application/json,text/json");
+                       
consumes("application/json,text/json,application/jcs+json");
                        validateEnd = env("JsonParser.validateEnd", false);
                }
 
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 2dfec57ff3..13c675e503 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
@@ -209,7 +209,7 @@ public class JsonSerializerSession extends 
WriterSerializerSession {
                this.ctx = builder.ctx;
        }
 
-       private SerializerWriter serializeBeanMap(JsonWriter out, BeanMap<?> m, 
String typeName) throws SerializeException {
+       protected SerializerWriter serializeBeanMap(JsonWriter out, BeanMap<?> 
m, String typeName) throws SerializeException {
                int i = indent;
                out.w('{');
 
@@ -282,7 +282,7 @@ public class JsonSerializerSession extends 
WriterSerializerSession {
                "rawtypes", // Raw types necessary for generic collection/map 
serialization
                "unchecked", // Type erasure requires unchecked casts in 
collection/map serialization
        })
-       private SerializerWriter serializeMap(JsonWriter out, Map m, 
ClassMeta<?> type) throws SerializeException {
+       protected SerializerWriter serializeMap(JsonWriter out, Map m, 
ClassMeta<?> type) throws SerializeException {
 
                var keyType = type.getKeyType();
                var valueType = type.getValueType();
@@ -326,7 +326,7 @@ public class JsonSerializerSession extends 
WriterSerializerSession {
         * @return The output target object wrapped in an {@link JsonWriter}.
         * @throws IOException Thrown by underlying stream.
         */
-       protected final JsonWriter getJsonWriter(SerializerPipe out) {
+       protected JsonWriter getJsonWriter(SerializerPipe out) {
                var output = out.getRawOutput();
                if (output instanceof JsonWriter output2)
                        return output2;
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/annotation/JcsConfig.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/annotation/JcsConfig.java
new file mode 100644
index 0000000000..19fa7f1461
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/annotation/JcsConfig.java
@@ -0,0 +1,53 @@
+/*
+ * 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.json.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * Annotation for specifying config properties for {@link 
org.apache.juneau.json.JcsSerializer}.
+ *
+ * <p>
+ * Used primarily for specifying bean configuration properties on REST classes 
and methods.
+ * JCS uses fixed canonical output, so no format-specific settings; the 
annotation provides
+ * consistency and future extensibility.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 8785 — JSON Canonicalization 
Scheme</a>
+ * </ul>
+ */
+@Target({ TYPE, METHOD })
+@Retention(RUNTIME)
+@Inherited
+@ContextApply(JcsConfigAnnotation.SerializerApply.class)
+public @interface JcsConfig {
+
+       /**
+        * Optional rank for this config.
+        *
+        * <p>
+        * Can be used to override default ordering and application of config 
annotations.
+        *
+        * @return The annotation value.
+        */
+       int rank() default 0;
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/annotation/JcsConfigAnnotation.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/annotation/JcsConfigAnnotation.java
new file mode 100644
index 0000000000..d91d489f02
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/annotation/JcsConfigAnnotation.java
@@ -0,0 +1,54 @@
+/*
+ * 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.json.annotation;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Utility classes and methods for the {@link JcsConfig @JcsConfig} annotation.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 8785 — JSON Canonicalization 
Scheme</a>
+ * </ul>
+ */
+public class JcsConfigAnnotation {
+
+       private JcsConfigAnnotation() {}
+
+       /**
+        * Applies {@link JcsConfig} annotations to a {@link 
JcsSerializer.Builder}.
+        */
+       public static class SerializerApply extends 
AnnotationApplier<JcsConfig,JcsSerializer.Builder> {
+
+               /**
+                * Constructor.
+                *
+                * @param vr The resolver for resolving values in annotations.
+                */
+               public SerializerApply(VarResolverSession vr) {
+                       super(JcsConfig.class, JcsSerializer.Builder.class, vr);
+               }
+
+               @Override
+               public void apply(AnnotationInfo<JcsConfig> ai, 
JcsSerializer.Builder b) {
+                       // No-op: JCS uses fixed canonical output; annotation 
provides consistency and future extensibility
+               }
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/package-info.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/package-info.java
index 0adf7b1105..11ea99b058 100755
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/package-info.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/package-info.java
@@ -15,6 +15,15 @@
  * limitations under the License.
  */
 /**
- * JSON Marshalling Support
+ * JSON Marshalling Support.
+ *
+ * <p>
+ * Includes support for:
+ * <ul>
+ *     <li>{@link org.apache.juneau.json.JsonSerializer} - Standard JSON 
serialization
+ *     <li>{@link org.apache.juneau.json.Json5Serializer} - JSON5 (simplified) 
serialization
+ *     <li>{@link org.apache.juneau.json.JcsSerializer} - JCS (RFC 8785) 
canonical JSON for hashing and signing
+ * </ul>
+ * </p>
  */
 package org.apache.juneau.json;
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Jcs.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Jcs.java
new file mode 100644
index 0000000000..f99870e1ae
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Jcs.java
@@ -0,0 +1,120 @@
+/*
+ * 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.marshaller;
+
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * A pairing of a {@link JcsSerializer} and {@link JsonParser} into a single 
class with
+ * convenience read/write methods.
+ *
+ * <p>
+ *     Produces canonical JSON per <a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 8785</a>.
+ *     Parsing uses the standard {@link JsonParser} since JCS output is valid 
JSON.
+ * </p>
+ *
+ * <h5 class='figure'>Examples:</h5>
+ * <p class='bjava'>
+ *     <jc>// Using static convenience methods.</jc>
+ *     String <jv>s</jv> = Jcs.<jsm>of</jsm>(<jv>myBean</jv>);
+ *     MyBean <jv>b</jv> = Jcs.<jsm>to</jsm>(<jv>s</jv>, 
MyBean.<jk>class</jk>);
+ * </p>
+ * <p class='bjava'>
+ *     <jc>// Using instance.</jc>
+ *     Jcs <jv>jcs</jv> = <jk>new</jk> Jcs();
+ *     String <jv>s</jv> = <jv>jcs</jv>.write(<jv>myBean</jv>);
+ *     MyBean <jv>b</jv> = <jv>jcs</jv>.read(<jv>s</jv>, 
MyBean.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='figure'>Example output (bean with name/age):</h5>
+ * <p class='bjson'>
+ *     {<jok>"age"</jok>:<jov>30</jov>,<jok>"name"</jok>:<jov>"Alice"</jov>}
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ *     <li class='note'>
+ *             {@link java.math.BigDecimal} and {@link java.math.BigInteger} 
values beyond IEEE 754 double
+ *             precision range will lose precision or throw during 
serialization.
+ *     <li class='note'>
+ *             {@link Double#NaN}, {@link Double#POSITIVE_INFINITY}, and 
{@link Double#NEGATIVE_INFINITY}
+ *             are not permitted and will throw during serialization.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 8785 — JSON Canonicalization 
Scheme</a>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/Jcs";>JCS topic</a>
+ *     <li class='jc'>{@link JcsSerializer}
+ *     <li class='jc'>{@link JsonParser}
+ * </ul>
+ */
+public class Jcs extends CharMarshaller {
+
+       /** Default reusable instance. */
+       public static final Jcs DEFAULT = new Jcs();
+
+       /**
+        * Serializes a Java object to a canonical JSON string.
+        *
+        * <p>
+        * A shortcut for calling 
<c><jsf>DEFAULT</jsf>.write(<jv>object</jv>)</c>.
+        *
+        * @param object The object to serialize.
+        * @return The serialized object.
+        * @throws SerializeException If a problem occurred trying to convert 
the output.
+        */
+       public static String of(Object object) throws SerializeException {
+               return DEFAULT.write(object);
+       }
+
+       /**
+        * Parses a canonical JSON string to the specified Java type.
+        *
+        * <p>
+        * A shortcut for calling <c><jsf>DEFAULT</jsf>.read(<jv>input</jv>, 
<jv>type</jv>)</c>.
+        *
+        * @param <T> The class type of the object being created.
+        * @param input The input string.
+        * @param type The object type to create.
+        * @return The parsed object.
+        * @throws ParseException Malformed input encountered.
+        */
+       public static <T> T to(String input, Class<T> type) throws 
ParseException {
+               return DEFAULT.read(input, type);
+       }
+
+       /**
+        * Constructor.
+        *
+        * <p>
+        * Uses {@link JcsSerializer#DEFAULT} and {@link JsonParser#DEFAULT}.
+        */
+       public Jcs() {
+               this(JcsSerializer.DEFAULT, JsonParser.DEFAULT);
+       }
+
+       /**
+        * Constructor.
+        *
+        * @param s The serializer to use for serializing output.
+        * @param p The parser to use for parsing input.
+        */
+       public Jcs(JcsSerializer s, JsonParser p) {
+               super(s, p);
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
index 8d36d4920d..f6bd8f43ba 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
@@ -3156,6 +3156,23 @@ public class RestClient extends BeanContextable 
implements HttpClient, Closeable
                        return 
serializer(Json5Serializer.class).parser(Json5Parser.class);
                }
 
+               /**
+                * Convenience method for specifying JCS (JSON Canonicalization 
Scheme, RFC 8785) as the marshalling transmission media type.
+                *
+                * <p>
+                *      {@link JcsSerializer} will be used to serialize POJOs 
to request bodies.
+                *      {@link JsonParser} will be used to parse POJOs from 
response bodies (JCS output is valid JSON).
+                * <p>
+                *      <c>Accept</c> and <c>Content-Type</c> will be set to 
<js>"application/jcs+json"</js>.
+                * <p>
+                *      Identical to calling 
<c>serializer(JcsSerializer.<jk>class</jk>).parser(JsonParser.<jk>class</jk>)</c>.
+                *
+                * @return This object.
+                */
+               public Builder jcs() {
+                       return 
serializer(JcsSerializer.class).parser(JsonParser.class);
+               }
+
                /**
                 * Assigns {@link ConnectionKeepAliveStrategy} instance.
                 *
@@ -5559,6 +5576,7 @@ public class RestClient extends BeanContextable 
implements HttpClient, Closeable
                        return
                                serializers(
                                        JsonSerializer.class,
+                                       JcsSerializer.class,
                                        Json5Serializer.class,
                                        JsonlSerializer.class,
                                        HtmlSerializer.class,
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
index ed03e27150..b236c3ce0e 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
@@ -1339,6 +1339,23 @@ public class RestRequest extends BeanSession implements 
HttpUriRequest, Configur
                return 
serializer(Json5Serializer.class).parser(Json5Parser.class);
        }
 
+       /**
+        * Convenience method for specifying JCS (JSON Canonicalization Scheme, 
RFC 8785) as the marshalling transmission media type for this request only.
+        *
+        * <p>
+        *      {@link JcsSerializer} will be used to serialize POJOs to 
request bodies.
+        *      {@link JsonParser} will be used to parse POJOs from response 
bodies (JCS output is valid JSON).
+        * <p>
+        *      <c>Accept</c> and <c>Content-Type</c> will be set to 
<js>"application/jcs+json"</js>.
+        * <p>
+        *      Identical to calling 
<c>serializer(JcsSerializer.<jk>class</jk>).parser(JsonParser.<jk>class</jk>)</c>.
+        *
+        * @return This object.
+        */
+       public RestRequest jcs() {
+               return serializer(JcsSerializer.class).parser(JsonParser.class);
+       }
+
        /**
         * Logs a message.
         *
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
index 4bdd2e0cae..7c4a96efe6 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
@@ -144,6 +144,7 @@ import org.apache.juneau.xml.*;
                HtmlSchemaDocSerializer.class,
                HjsonSerializer.class,
                JsonSerializer.class,
+               JcsSerializer.class,
                Json5Serializer.class,
                JsonlSerializer.class,
                JsonSchemaSerializer.class,
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripAddClassAttrs_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripAddClassAttrs_Test.java
index 96ed3f3e67..22eea9479b 100755
--- 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripAddClassAttrs_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripAddClassAttrs_Test.java
@@ -149,6 +149,11 @@ class RoundTripAddClassAttrs_Test extends TestBase {
                        
.serializer(HjsonSerializer.create().ws().addBeanTypes().addRootType())
                        .parser(HjsonParser.create().disableInterfaceProxies())
                        .build(),
+               tester(26, "Jcs - default")
+                       
.serializer(JcsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create().disableInterfaceProxies())
+                       .skipIf(o -> o instanceof Double d && (d.isNaN() || 
d.isInfinite()))
+                       .build(),
        };
 
        static RoundTrip_Tester[]  testers() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeanMaps_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeanMaps_Test.java
index bfb9b16ede..3c964bc2de 100755
--- 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeanMaps_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeanMaps_Test.java
@@ -151,6 +151,11 @@ class RoundTripBeanMaps_Test extends TestBase {
                        
.serializer(HjsonSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
                        .parser(HjsonParser.create().disableInterfaceProxies())
                        .build(),
+               tester(24, "Jcs - default")
+                       
.serializer(JcsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create())
+                       .skipIf(o -> o instanceof Double d && (d.isNaN() || 
d.isInfinite()))
+                       .build(),
        };
 
        static RoundTrip_Tester[]  testers() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeansWithBuilders_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeansWithBuilders_Test.java
index a47966e84a..c742609dc3 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeansWithBuilders_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeansWithBuilders_Test.java
@@ -145,6 +145,11 @@ class RoundTripBeansWithBuilders_Test extends TestBase {
                        
.serializer(HjsonSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
                        .parser(HjsonParser.create())
                        .build(),
+               tester(24, "Jcs - default")
+                       
.serializer(JcsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create())
+                       .skipIf(o -> o instanceof Double d && (d.isNaN() || 
d.isInfinite()))
+                       .build(),
        };
 
        static RoundTrip_Tester[] testers() {
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
index 8a770998da..9eeef685e0 100644
--- 
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
@@ -199,6 +199,11 @@ class RoundTripDateTime_Test extends TestBase {
                        
.serializer(HjsonSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
                        .parser(HjsonParser.create())
                        .build(),
+               tester(37, "Jcs - default")
+                       
.serializer(JcsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create())
+                       .skipIf(o -> o instanceof Double d && (d.isNaN() || 
d.isInfinite()))
+                       .build(),
        };
 
        static RoundTrip_Tester[] testers() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripLargeObjects_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripLargeObjects_Test.java
index c834bb5900..4c5ed1d13a 100755
--- 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripLargeObjects_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripLargeObjects_Test.java
@@ -158,6 +158,11 @@ class RoundTripLargeObjects_Test extends TestBase {
                tester(26, "Hjson - default")
                        
.serializer(HjsonSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
                        .parser(HjsonParser.create())
+                       .build(),
+               tester(27, "Jcs - default")
+                       
.serializer(JcsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create())
+                       .skipIf(o -> o instanceof Double d && (d.isNaN() || 
d.isInfinite()))
                        .build()
        };
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripMaps_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripMaps_Test.java
index d9e89d1f18..e6d90e6d3f 100755
--- 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripMaps_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripMaps_Test.java
@@ -193,6 +193,11 @@ class RoundTripMaps_Test extends TestBase {
                        
.serializer(HjsonSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
                        .parser(HjsonParser.create())
                        .build(),
+               tester(34, "Jcs - default")
+                       
.serializer(JcsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create())
+                       .skipIf(o -> o instanceof Double d && (d.isNaN() || 
d.isInfinite()))
+                       .build(),
        };
 
        static RoundTrip_Tester[]  testers() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
index 9aa9052b2f..2a724bcbdf 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
@@ -148,6 +148,11 @@ public abstract class RoundTripTest_Base extends TestBase {
                        
.serializer(HjsonSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
                        .parser(HjsonParser.create())
                        .build(),
+               tester(25, "Jcs - default")
+                       
.serializer(JcsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create())
+                       .skipIf(o -> o instanceof Double d && (d.isNaN() || 
d.isInfinite()))
+                       .build(),
        };
 
        static RoundTrip_Tester[]  testers() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTransformBeans_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTransformBeans_Test.java
index 8f7a1ae1f6..d603fd95e0 100755
--- 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTransformBeans_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTransformBeans_Test.java
@@ -198,6 +198,12 @@ class RoundTripTransformBeans_Test extends TestBase {
                        .parser(HjsonParser.create())
                        .skipIf(o -> o instanceof A)  // byte[][] not yet 
serialized as Base64 in inline JSON5
                        .build(),
+               tester(33, "Jcs - default")
+                       
.serializer(JcsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+                       .parser(JsonParser.create())
+                       .skipIf(o -> o instanceof Double d && (d.isNaN() || 
d.isInfinite()))
+                       .skipIf(o -> o instanceof A)  // byte[][] not yet 
serialized as Base64 in inline JSON5
+                       .build(),
        };
 
        static RoundTrip_Tester[]  testers() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/json/JcsCanonical_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/json/JcsCanonical_Test.java
new file mode 100644
index 0000000000..96e061b68b
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/json/JcsCanonical_Test.java
@@ -0,0 +1,131 @@
+/*
+ * 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.json;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.charset.*;
+import java.security.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+class JcsCanonical_Test extends TestBase {
+
+       @Test
+       void d01_rfcExample1() throws Exception {
+               // RFC 8785 Section 3.2.2: numbers, string, literals
+               var numbers = list(333333333.33333329, 1E30, 4.50, 2e-3, 
0.000000000000000000000000001);
+               var s = Character.toString((char) 0x20AC) + "$" + 
Character.toString((char) 0x000F) + "\nA'B\"\\\\\"/";
+               var literals = list((Object) null, true, false);
+               var m = JsonMap.of("numbers", numbers, "string", s, "literals", 
literals);
+               var out = JcsSerializer.DEFAULT.serialize(m);
+               // Keys sorted: literals, numbers, string
+               assertTrue(out.startsWith("{\"literals\":"));
+               assertTrue(out.contains("\"numbers\":"));
+               assertTrue(out.contains("\"string\":"));
+               assertFalse(out.contains(" "));
+       }
+
+       @Test
+       void d02_rfcExample2Sorted() throws Exception {
+               // Sorted version per RFC 3.2.3
+               var numbers = list(333333333.33333329, 1E30, 4.50, 2e-3, 
0.000000000000000000000000001);
+               var literals = list((Object) null, true, false);
+               var m = JsonMap.of("literals", literals, "numbers", numbers);
+               var out = JcsSerializer.DEFAULT.serialize(m);
+               
assertTrue(out.startsWith("{\"literals\":[null,true,false],\"numbers\":"));
+       }
+
+       @Test
+       void d03_noWhitespace() throws Exception {
+               var m = JsonMap.of("a", 1, "b", 2);
+               var s = JcsSerializer.DEFAULT.serialize(m);
+               assertFalse(s.contains(" "));
+               assertFalse(s.contains("\n"));
+               assertFalse(s.contains("\t"));
+       }
+
+       @Test
+       void d04_simpleBeanCanonical() throws Exception {
+               var m = JsonMap.of("name", "Alice", "age", 30);
+               var s = JcsSerializer.DEFAULT.serialize(m);
+               assertEquals("{\"age\":30,\"name\":\"Alice\"}", s);
+       }
+
+       @Test
+       void d05_nestedBeanCanonical() throws Exception {
+               var inner = JsonMap.of("zip", "80201", "city", "Denver");
+               var outer = JsonMap.of("address", inner, "name", "Alice");
+               var s = JcsSerializer.DEFAULT.serialize(outer);
+               
assertTrue(s.contains("\"address\":{\"city\":\"Denver\",\"zip\":\"80201\"}"));
+               assertTrue(s.contains("\"name\":\"Alice\""));
+       }
+
+       @Test
+       void d06_mixedTypesCanonical() throws Exception {
+               var m = JsonMap.of("n", 42, "s", "hi", "b", true, "x", (Object) 
null, "a", list(1, 2));
+               var s = JcsSerializer.DEFAULT.serialize(m);
+               assertFalse(s.contains(" "));
+               // All keys present, sorted
+               assertTrue(s.contains("\"a\":"));
+               assertTrue(s.contains("\"b\":true"));
+               assertTrue(s.contains("\"n\":42"));
+               assertTrue(s.contains("\"s\":\"hi\""));
+               assertTrue(s.contains("\"x\":null"));
+       }
+
+       @Test
+       void d07_deterministicRoundTrip() throws Exception {
+               var m = JsonMap.of("z", 3, "a", 1, "m", 2);
+               var s1 = JcsSerializer.DEFAULT.serialize(m);
+               var s2 = JcsSerializer.DEFAULT.serialize(m);
+               assertEquals(s1, s2);
+       }
+
+       @Test
+       void d08_hashStability() throws Exception {
+               var m = JsonMap.of("c", 3, "a", 1, "b", 2);
+               var s1 = JcsSerializer.DEFAULT.serialize(m);
+               var s2 = JcsSerializer.DEFAULT.serialize(m);
+               var md = MessageDigest.getInstance("SHA-256");
+               var bytes1 = s1.getBytes(StandardCharsets.UTF_8);
+               var bytes2 = s2.getBytes(StandardCharsets.UTF_8);
+               assertArrayEquals(bytes1, bytes2);
+               assertEquals(bytesToHex(md.digest(bytes1)), 
bytesToHex(md.digest(bytes2)));
+       }
+
+       @Test
+       void d09_emptyObject() throws Exception {
+               assertEquals("{}", 
JcsSerializer.DEFAULT.serialize(JsonMap.of()));
+       }
+
+       @Test
+       void d10_alphabeticalOrder() throws Exception {
+               var m = JsonMap.of("c", 3, "a", 1, "b", 2);
+               assertEquals("{\"a\":1,\"b\":2,\"c\":3}", 
JcsSerializer.DEFAULT.serialize(m));
+       }
+
+       private static String bytesToHex(byte[] bytes) {
+               var sb = new StringBuilder();
+               for (var b : bytes)
+                       sb.append(String.format("%02x", b));
+               return sb.toString();
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/json/JcsEdgeCases_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/json/JcsEdgeCases_Test.java
new file mode 100644
index 0000000000..7d4ef050e9
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/json/JcsEdgeCases_Test.java
@@ -0,0 +1,99 @@
+/*
+ * 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.json;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Edge case tests for JCS serialization.
+ */
+class JcsEdgeCases_Test extends TestBase {
+
+       @Test
+       void f01_emptyBean() throws Exception {
+               assertEquals("{}", 
JcsSerializer.DEFAULT.serialize(JsonMap.of()));
+       }
+
+       @Test
+       void f02_nullRoot() throws Exception {
+               assertEquals("null", JcsSerializer.DEFAULT.serialize(null));
+       }
+
+       @Test
+       void f03_booleanRoot() throws Exception {
+               assertEquals("true", JcsSerializer.DEFAULT.serialize(true));
+               assertEquals("false", JcsSerializer.DEFAULT.serialize(false));
+       }
+
+       @Test
+       void f04_numberRoot() throws Exception {
+               assertEquals("42", JcsSerializer.DEFAULT.serialize(42));
+               assertEquals("3.14", JcsSerializer.DEFAULT.serialize(3.14));
+       }
+
+       @Test
+       void f05_stringRoot() throws Exception {
+               assertEquals("\"hello\"", 
JcsSerializer.DEFAULT.serialize("hello"));
+       }
+
+       @Test
+       void f06_arrayRoot() throws Exception {
+               var a = list(1, 2, 3);
+               assertEquals("[1,2,3]", JcsSerializer.DEFAULT.serialize(a));
+       }
+
+       @Test
+       void f07_deepNesting() throws Exception {
+               var deep = JsonMap.of("a", JsonMap.of("b", JsonMap.of("c", 
JsonMap.of("d", JsonMap.of("e", 1)))));
+               assertEquals("{\"a\":{\"b\":{\"c\":{\"d\":{\"e\":1}}}}}", 
JcsSerializer.DEFAULT.serialize(deep));
+       }
+
+       @Test
+       void f08_duplicateMapKeys() throws Exception {
+               // JSON semantics: last value wins. JCS output is sorted.
+               var m = new LinkedHashMap<String, Integer>();
+               m.put("x", 1);
+               m.put("a", 2);
+               m.put("x", 3);  // overwrites
+               assertEquals("{\"a\":2,\"x\":3}", 
JcsSerializer.DEFAULT.serialize(m));
+       }
+
+       @Test
+       void f09_largeObject() throws Exception {
+               var m = new TreeMap<String, Integer>();
+               for (var i = 0; i < 100; i++)
+                       m.put("k" + i, i);
+               var s = JcsSerializer.DEFAULT.serialize(m);
+               // All keys should be sorted (k0, k1, ..., k9, k10, ..., k99)
+               assertTrue(s.startsWith("{\"k0\":0,\"k1\":1"));
+               assertTrue(s.contains("\"k99\":99"));
+               assertFalse(s.contains(" "));
+       }
+
+       @Test
+       void f10_emptyStrings() throws Exception {
+               var m = JsonMap.of("", "emptyVal", "emptyKey", "");
+               assertEquals("{\"\":\"emptyVal\",\"emptyKey\":\"\"}", 
JcsSerializer.DEFAULT.serialize(m));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/json/JcsNumbers_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/json/JcsNumbers_Test.java
new file mode 100644
index 0000000000..e84a5d2a44
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/json/JcsNumbers_Test.java
@@ -0,0 +1,134 @@
+/*
+ * 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.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.serializer.*;
+import org.junit.jupiter.api.*;
+
+class JcsNumbers_Test extends TestBase {
+
+       @Test
+       void a01_zero() throws Exception {
+               assertEquals("0", JcsSerializer.DEFAULT.serialize(0.0));
+       }
+
+       @Test
+       void a02_negativeZero() throws Exception {
+               assertEquals("0", JcsSerializer.DEFAULT.serialize(-0.0));
+       }
+
+       @Test
+       void a03_minPosNumber() throws Exception {
+               // Double.MIN_VALUE is the smallest positive double (~4.9e-324)
+               assertEquals("4.9e-324", 
JcsSerializer.DEFAULT.serialize(Double.MIN_VALUE));
+       }
+
+       @Test
+       void a04_minNegNumber() throws Exception {
+               assertEquals("-4.9e-324", 
JcsSerializer.DEFAULT.serialize(-Double.MIN_VALUE));
+       }
+
+       @Test
+       void a05_maxPosNumber() throws Exception {
+               assertEquals("1.7976931348623157e+308", 
JcsSerializer.DEFAULT.serialize(Double.MAX_VALUE));
+       }
+
+       @Test
+       void a06_maxNegNumber() throws Exception {
+               assertEquals("-1.7976931348623157e+308", 
JcsSerializer.DEFAULT.serialize(-Double.MAX_VALUE));
+       }
+
+       @Test
+       void a07_maxPosInt() throws Exception {
+               assertEquals("9007199254740992", 
JcsSerializer.DEFAULT.serialize(9007199254740992L));
+       }
+
+       @Test
+       void a08_maxNegInt() throws Exception {
+               assertEquals("-9007199254740992", 
JcsSerializer.DEFAULT.serialize(-9007199254740992L));
+       }
+
+       @Test
+       void a09_largeInteger() throws Exception {
+               // In [1e-6, 1e21) so ECMAScript uses fixed notation
+               assertEquals("295147905179352830000", 
JcsSerializer.DEFAULT.serialize(295147905179352830000.0));
+       }
+
+       @Test
+       void a10_noTrailingZeros() throws Exception {
+               assertEquals("4.5", JcsSerializer.DEFAULT.serialize(4.50));
+       }
+
+       @Test
+       void a11_scientificNotation() throws Exception {
+               // RFC 8785: 1E30 → 1e+30
+               assertEquals("1e+30", JcsSerializer.DEFAULT.serialize(1E30));
+       }
+
+       @Test
+       void a12_smallDecimal() throws Exception {
+               assertEquals("0.002", JcsSerializer.DEFAULT.serialize(2e-3));
+       }
+
+       @Test
+       void a13_verySmallDecimal() throws Exception {
+               // RFC 8785: 0.000...001 → 1e-27
+               assertEquals("1e-27", 
JcsSerializer.DEFAULT.serialize(0.000000000000000000000000001));
+       }
+
+       @Test
+       void a14_rejectNaN() {
+               assertThrows(SerializeException.class, () -> 
JcsSerializer.DEFAULT.serialize(Double.NaN));
+       }
+
+       @Test
+       void a15_rejectInfinity() {
+               assertThrows(SerializeException.class, () -> 
JcsSerializer.DEFAULT.serialize(Double.POSITIVE_INFINITY));
+       }
+
+       @Test
+       void a16_rejectNegInfinity() {
+               assertThrows(SerializeException.class, () -> 
JcsSerializer.DEFAULT.serialize(Double.NEGATIVE_INFINITY));
+       }
+
+       @Test
+       void a17_integerNoDecimalPoint() throws Exception {
+               assertEquals("42", JcsSerializer.DEFAULT.serialize(42.0));
+       }
+
+       @Test
+       void a18_roundToEven() throws Exception {
+               // In [1e-6, 1e21) so ECMAScript uses fixed notation
+               assertEquals("1424953923781206.2", 
JcsSerializer.DEFAULT.serialize(1424953923781206.2));
+       }
+
+       @Test
+       void a19_edgeCasePrecision() throws Exception {
+               // RFC 8785 Section 3.2.2: 333333333.33333329 → 
333333333.3333333
+               assertEquals("333333333.3333333", 
JcsSerializer.DEFAULT.serialize(333333333.33333329));
+       }
+
+       @Test
+       void a20_floatValues() throws Exception {
+               // Float promoted to double; 42f serializes as integer
+               assertEquals("42", JcsSerializer.DEFAULT.serialize(42f));
+               assertEquals("0", JcsSerializer.DEFAULT.serialize(0f));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/json/JcsSorting_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/json/JcsSorting_Test.java
new file mode 100644
index 0000000000..63170fe412
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/json/JcsSorting_Test.java
@@ -0,0 +1,116 @@
+/*
+ * 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.json;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for JCS property/key sorting per RFC 8785 (UTF-16 code unit order).
+ */
+class JcsSorting_Test extends TestBase {
+
+       @Test
+       void c01_alphabeticalOrder() throws Exception {
+               var m = JsonMap.of("a", 1, "b", 2, "c", 3);
+               assertEquals("{\"a\":1,\"b\":2,\"c\":3}", 
JcsSerializer.DEFAULT.serialize(m));
+       }
+
+       @Test
+       void c02_reverseInputOrder() throws Exception {
+               var m = JsonMap.of("c", 3, "b", 2, "a", 1);
+               assertEquals("{\"a\":1,\"b\":2,\"c\":3}", 
JcsSerializer.DEFAULT.serialize(m));
+       }
+
+       @Test
+       void c03_nestedObjectsSorted() throws Exception {
+               var inner = JsonMap.of("z", 1, "y", 2, "x", 3);
+               var outer = JsonMap.of("b", inner, "a", "top");
+               // Outer keys sorted: a, b. Inner keys sorted: x, y, z.
+               assertEquals("{\"a\":\"top\",\"b\":{\"x\":3,\"y\":2,\"z\":1}}", 
JcsSerializer.DEFAULT.serialize(outer));
+       }
+
+       @Test
+       void c04_arrayOrderPreserved() throws Exception {
+               var m = JsonMap.of("arr", list(3, 1, 2));
+               // Array order preserved; object keys sorted
+               assertEquals("{\"arr\":[3,1,2]}", 
JcsSerializer.DEFAULT.serialize(m));
+       }
+
+       @Test
+       void c05_utf16SortOrder() throws Exception {
+               // RFC 8785: keys sorted by UTF-16 code unit order
+               var m = new LinkedHashMap<String, Integer>();
+               m.put(Character.toString((char) 0x00F6), 1);   // ö
+               m.put("1", 2);
+               m.put("\r", 3);  // carriage return
+               m.put(Character.toString((char) 0x20AC), 4);   // €
+               m.put(Character.toString((char) 0x80), 5);    // C1 control
+               m.put(Character.toString((char) 0xFB33), 6);  // Hebrew letter
+               var s = JcsSerializer.DEFAULT.serialize(m);
+               // Parse and verify key order: \r < 1 < U+0080 < ö < € < Hebrew
+               var parsed = JsonParser.DEFAULT.parse(s, JsonMap.class);
+               var keys = parsed.keySet().stream().toList();
+               assertEquals(6, keys.size());
+               assertEquals("\r", keys.get(0));
+               assertEquals("1", keys.get(1));
+               assertEquals(Character.toString((char) 0x80), keys.get(2));
+               assertEquals(Character.toString((char) 0x00F6), keys.get(3));
+               assertEquals(Character.toString((char) 0x20AC), keys.get(4));
+               assertEquals(Character.toString((char) 0xFB33), keys.get(5));
+       }
+
+       @Test
+       void c06_mapKeysSorted() throws Exception {
+               var m = new HashMap<String, Object>();
+               m.put("z", 1);
+               m.put("a", 2);
+               m.put("m", 3);
+               assertEquals("{\"a\":2,\"m\":3,\"z\":1}", 
JcsSerializer.DEFAULT.serialize(m));
+       }
+
+       @Test
+       void c07_emptyObject() throws Exception {
+               assertEquals("{}", 
JcsSerializer.DEFAULT.serialize(JsonMap.of()));
+       }
+
+       @Test
+       void c08_singleProperty() throws Exception {
+               var m = JsonMap.of("a", 1);
+               assertEquals("{\"a\":1}", JcsSerializer.DEFAULT.serialize(m));
+       }
+
+       @Test
+       void c09_numericStringKeys() throws Exception {
+               // Lexicographic (UTF-16), not numeric: "1" < "10" < "2"
+               var m = JsonMap.of("10", 1, "2", 2, "1", 3);
+               assertEquals("{\"1\":3,\"10\":1,\"2\":2}", 
JcsSerializer.DEFAULT.serialize(m));
+       }
+
+       @Test
+       void c10_caseSensitive() throws Exception {
+               // UTF-16: "A" (U+0041) < "a" (U+0061)
+               var m = JsonMap.of("a", 1, "A", 2, "B", 3, "b", 4);
+               assertEquals("{\"A\":2,\"B\":3,\"a\":1,\"b\":4}", 
JcsSerializer.DEFAULT.serialize(m));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/json/JcsStrings_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/json/JcsStrings_Test.java
new file mode 100644
index 0000000000..973af41eeb
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/json/JcsStrings_Test.java
@@ -0,0 +1,100 @@
+/*
+ * 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.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.serializer.*;
+import org.junit.jupiter.api.*;
+
+class JcsStrings_Test extends TestBase {
+
+       @Test
+       void b01_simpleString() throws Exception {
+               assertEquals("{\"x\":\"hello\"}", 
JcsSerializer.DEFAULT.serialize(JsonMap.of("x", "hello")));
+       }
+
+       @Test
+       void b02_escapeBackslash() throws Exception {
+               assertEquals("{\"x\":\"\\\\\\\\\"}", 
JcsSerializer.DEFAULT.serialize(JsonMap.of("x", "\\\\")));
+       }
+
+       @Test
+       void b03_escapeQuote() throws Exception {
+               var m = JsonMap.of("x", "\"");
+               assertTrue(JcsSerializer.DEFAULT.serialize(m).contains("\\\""));
+       }
+
+       @Test
+       void b04_controlChars() throws Exception {
+               var m = JsonMap.of("x", "\t\n");
+               var s = JcsSerializer.DEFAULT.serialize(m);
+               assertTrue(s.contains("\\t"));
+               assertTrue(s.contains("\\n"));
+       }
+
+       @Test
+       void b05_otherControlChars() throws Exception {
+               // U+000F (non-predefined) uses lowercase \u000f
+               var m = JsonMap.of("x", Character.toString((char) 0x000F));
+               var s = JcsSerializer.DEFAULT.serialize(m);
+               assertTrue(s.contains("\\u000f"));
+       }
+
+       @Test
+       void b06_nonAsciiLiteral() throws Exception {
+               // € (U+20AC) output as literal UTF-8 per RFC 8785
+               var m = JsonMap.of("x", Character.toString((char) 0x20AC));
+               var s = JcsSerializer.DEFAULT.serialize(m);
+               assertTrue(s.contains(Character.toString((char) 0x20AC)));
+       }
+
+       @Test
+       void b07_rfcStringExample() throws Exception {
+               // RFC 8785 Section 3.2.2: €$\u000f\nA'B\"\\\/
+               var s = Character.toString((char) 0x20AC) + "$" + 
Character.toString((char) 0x000F) + "\nA'B\"\\\\\"/";
+               var m = JsonMap.of("string", s);
+               var out = JcsSerializer.DEFAULT.serialize(m);
+               assertTrue(out.contains("\\u000f"));
+               assertTrue(out.contains("\\n"));
+               assertTrue(out.contains("\\\\"));
+       }
+
+       @Test
+       void b08_emojiLiteral() throws Exception {
+               // Emoji (surrogate pair) output as literal UTF-8 per RFC 8785
+               var emoji = Character.toString(Character.toCodePoint('\uD83D', 
'\uDE00'));
+               var m = JsonMap.of("x", emoji);
+               var s = JcsSerializer.DEFAULT.serialize(m);
+               // Round-trip: parse back and verify
+               var parsed = JsonParser.DEFAULT.parse(s, JsonMap.class);
+               assertEquals(emoji, parsed.getString("x"));
+       }
+
+       @Test
+       void b09_loneSurrogateError() {
+               var s = "\uDEAD";
+               assertThrows(SerializeException.class, () -> 
JcsSerializer.DEFAULT.serialize(JsonMap.of("x", s)));
+       }
+
+       @Test
+       void b10_emptyString() throws Exception {
+               assertEquals("{\"x\":\"\"}", 
JcsSerializer.DEFAULT.serialize(JsonMap.of("x", "")));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/marshaller/Jcs_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/marshaller/Jcs_Test.java
new file mode 100644
index 0000000000..09633a219b
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/marshaller/Jcs_Test.java
@@ -0,0 +1,55 @@
+/*
+ * 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.marshaller;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+class Jcs_Test extends TestBase {
+
+       @Test
+       void e01_of() throws Exception {
+               var m = JsonMap.of("name", "Alice", "age", 30);
+               var s = Jcs.of(m);
+               assertEquals("{\"age\":30,\"name\":\"Alice\"}", s);
+       }
+
+       @Test
+       void e02_to() throws Exception {
+               var s = "{\"age\":30,\"name\":\"Alice\"}";
+               var m = Jcs.to(s, JsonMap.class);
+               assertEquals(30, m.getInt("age"));
+               assertEquals("Alice", m.getString("name"));
+       }
+
+       @Test
+       void e03_roundTrip() throws Exception {
+               var m = JsonMap.of("a", 1, "b", 2, "c", 3);
+               var s = Jcs.of(m);
+               var m2 = Jcs.to(s, JsonMap.class);
+               assertEquals(m, m2);
+       }
+
+       @Test
+       void e04_defaultInstance() throws Exception {
+               var m = JsonMap.of("x", 1);
+               assertEquals("{\"x\":1}", Jcs.DEFAULT.write(m));
+       }
+}
diff --git a/todo/3_jcs_implementation.md b/todo/3_jcs_implementation.md
deleted file mode 100644
index c32b78f41e..0000000000
--- a/todo/3_jcs_implementation.md
+++ /dev/null
@@ -1,666 +0,0 @@
-# JCS (JSON Canonicalization Scheme) Support Implementation Plan for Apache 
Juneau
-
-## Overview
-
-This plan covers implementing full marshalling support for JCS (JSON 
Canonicalization Scheme), defined by RFC 8785. JCS produces a deterministic, 
byte-for-byte canonical representation of JSON data, enabling reliable 
cryptographic operations (hashing, signing) on JSON without converting to an 
opaque format like base64.
-
-The `Jcs` marshaller pairs `JcsSerializer` with the standard `JsonParser`, 
enabling full round-trip serialization and deserialization at the same level as 
JSON. No separate `JcsParser` class is needed because JCS output is valid JSON 
and can be parsed directly by `JsonParser`. The implementation extends 
`JsonSerializer` to enforce the strict canonicalization rules.
-
-## Specification
-
-- **RFC**: https://www.rfc-editor.org/rfc/rfc8785
-- **Media type**: `application/json` (canonical JSON is standard JSON)
-- **Encoding**: UTF-8
-- **I-JSON**: RFC 7493 (required subset)
-
----
-
-## JCS Canonicalization Rules
-
-### 1. No Whitespace
-
-No whitespace between tokens. Output is a single contiguous line.
-
-```
-{"age":42,"name":"John"}
-```
-
-Not:
-```
-{ "age": 42, "name": "John" }
-```
-
-### 2. Sorted Object Properties
-
-Properties are sorted by their **raw** (unescaped) Unicode values, using 
UTF-16 code unit comparison (matching ECMAScript's `Array.prototype.sort()`). 
Sorting is applied recursively to all nested objects. Array element order is 
preserved.
-
-```json
-{"a":1,"b":2,"c":3}
-```
-
-Not:
-```json
-{"c":3,"a":1,"b":2}
-```
-
-Sort order for non-ASCII keys follows UTF-16 code unit ordering:
-```
-U+000D (Carriage Return) < U+0031 ("1") < U+0080 (Control) < U+00F6 ("ö") < 
U+20AC ("€") < U+D83D+DE00 (Emoji) < U+FB33 (Hebrew)
-```
-
-### 3. ECMAScript Number Serialization
-
-Numbers are serialized according to ECMAScript's `JSON.stringify()` rules 
(Section 7.1.12.1 of ECMA-262):
-
-- No leading zeros: `0.5` not `0.50`
-- No trailing zeros: `4.5` not `4.50`
-- No unnecessary plus sign in exponent: `1e+30` (but this is ECMAScript's 
output)
-- Integer values: no decimal point (`42` not `42.0`)
-- Negative zero: serialized as `0` (not `-0`)
-- Shortest representation that round-trips correctly
-- NaN and Infinity are **errors** (not permitted)
-
-Examples from the RFC:
-
-| IEEE 754 Double | JCS Output |
-|----------------|-----------|
-| 0.0 | `0` |
-| -0.0 | `0` |
-| 5e-324 | `5e-324` |
-| 1.7976931348623157e+308 | `1.7976931348623157e+308` |
-| 333333333.33333329 | `333333333.3333333` |
-| 1E30 | `1e+30` |
-| 4.50 | `4.5` |
-| 2e-3 | `0.002` |
-| 0.000000000000000000000000001 | `1e-27` |
-
-### 4. ECMAScript String Serialization
-
-Strings follow ECMAScript's `JSON.stringify()`:
-
-- Always double-quoted
-- Mandatory escapes: `\\` and `\"`
-- Control characters U+0000-U+001F: use `\b`, `\t`, `\n`, `\f`, `\r` for 
predefined escapes; `\uHHHH` (lowercase hex) for others
-- All characters outside the control range are serialized as-is (no 
unnecessary `\uHHHH` escaping)
-- Lone surrogates (e.g., U+DEAD) cause an error
-
-### 5. Literal Serialization
-
-`null`, `true`, `false` serialized exactly as-is (already deterministic).
-
-### 6. UTF-8 Output
-
-Output MUST be encoded in UTF-8.
-
----
-
-## Architecture
-
-JCS is a variant of JSON serialization, so it extends `JsonSerializer` -- 
similar to how `Json5Serializer` extends `JsonSerializer`.
-
-```
-JsonSerializer
-  └── JcsSerializer
-        └── JcsSerializerSession
-              └── JcsWriter (extends JsonWriter)
-
-Jcs (marshaller)
-  ├── JcsSerializer  (serialization)
-  └── JsonParser     (parsing — JCS output is valid JSON)
-```
-
-The `Jcs` marshaller pairs `JcsSerializer` with the existing `JsonParser` for 
full round-trip support. No separate `JcsParser` class is needed.
-
----
-
-## Limitations
-
-The following spec-level constraints must be **documented in the Javadoc** of 
`JcsSerializer` and `Jcs`, and must be accounted for in round-trip tests (e.g., 
via `skipIf` predicates where the values cannot be round-tripped):
-
-| Limitation | Detail |
-|---|---|
-| **`BigDecimal` / `BigInteger` precision** | RFC 8785 requires all numbers to 
fit in IEEE 754 double precision. `BigDecimal` values with more significant 
digits than a `double` can hold will either lose precision silently or throw a 
`SerializeException`. This is a spec-level constraint, not an implementation 
gap. |
-| **`NaN` and `Infinity`** | `Double.NaN`, `Double.POSITIVE_INFINITY`, and 
`Double.NEGATIVE_INFINITY` are rejected per RFC 8785 and will throw a 
`SerializeException`. |
-| **Lone surrogates** | Strings containing lone UTF-16 surrogate code units 
(e.g., U+DEAD without a paired surrogate) are rejected per the spec and will 
throw a `SerializeException`. |
-| **Media type** | JCS does not define its own media type. Output is 
`application/json`. Do not register `JcsSerializer` as a content-negotiation 
alternative to `JsonSerializer`; use it explicitly by class reference. |
-
-### Round-Trip Compatibility
-
-For all other data structures supported by JSON, JCS provides full round-trip 
support via `JsonParser`. This includes:
-- Primitives and their wrappers (`String`, `int`, `long`, `double`, `boolean`, 
etc.)
-- Arrays (1D, 2D, 3D)
-- Collections (`List`, `Set`, `Queue`, etc.)
-- Maps (`Map<K,V>`, including `TreeMap`, `LinkedHashMap`, etc.)
-- Beans and records
-- Enums
-- Date/time types (`Date`, `Calendar`, `Instant`, `ZonedDateTime`, 
`LocalDate`, etc.)
-- `Optional<T>`
-- `Iterator`, `Iterable`, `Stream` (serialization only — consumed once)
-
----
-
-## Files to Create
-
-All source files in 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json/`.
-
-### 1. `JcsSerializer.java`
-
-Extends `JsonSerializer`.
-
-```java
-package org.apache.juneau.json;
-
-public class JcsSerializer extends JsonSerializer {
-
-    public static final JcsSerializer DEFAULT = ...;
-
-    public static class Builder extends JsonSerializer.Builder {
-        protected Builder() {
-            super();
-            // Override parent defaults for canonical output
-            sortProperties();     // Sort bean properties alphabetically
-            quoteChar('"');       // Always double quotes
-            simpleAttrs(false);   // Never unquoted attributes
-            ws(false);            // No whitespace
-        }
-    }
-
-    @Override
-    public JcsSerializerSession.Builder createSession() {
-        return JcsSerializerSession.create(this);
-    }
-}
-```
-
-Key: The builder pre-configures `sortProperties()` and forces strict JSON 
output (no JSON5 relaxations). The heavy lifting is in the session.
-
-### 2. `JcsSerializerSession.java`
-
-Extends `JsonSerializerSession`. Overrides key serialization behaviors.
-
-```java
-package org.apache.juneau.json;
-
-public class JcsSerializerSession extends JsonSerializerSession {
-
-    @Override
-    protected void doSerialize(SerializerPipe out, Object o) throws 
IOException, SerializeException {
-        // Same as parent but with canonical writer
-        serializeAnything(getJcsWriter(out), o, getExpectedRootType(o), 
"root", null);
-    }
-
-    // Override map serialization to sort keys by UTF-16 code unit comparison
-    @Override
-    protected void serializeMap(JsonWriter out, Map m, ClassMeta type)
-            throws IOException, SerializeException {
-        // Sort map keys using JCS UTF-16 code unit comparison
-        List<Map.Entry> sorted = new ArrayList<>(m.entrySet());
-        sorted.sort((a, b) -> jcsCompare(toString(a.getKey()), 
toString(b.getKey())));
-        // Serialize in sorted order
-        out.append('{');
-        boolean first = true;
-        for (Map.Entry e : sorted) {
-            if (!first) out.append(',');
-            first = false;
-            serializeString(out, toString(e.getKey()));
-            out.append(':');
-            serializeAnything(out, e.getValue(), ...);
-        }
-        out.append('}');
-    }
-
-    // Override bean serialization to sort properties by UTF-16 code unit 
comparison
-    @Override
-    protected void serializeBeanMap(JsonWriter out, BeanMap m, String typeName)
-            throws IOException, SerializeException {
-        // Collect all properties, sort by name using JCS comparison, serialize
-    }
-
-    // Override number serialization for ECMAScript compliance
-    @Override
-    protected void serializeNumber(JsonWriter out, Number n)
-            throws IOException, SerializeException {
-        // Implement ECMAScript-compatible number serialization
-        // - No trailing zeros
-        // - Shortest representation that round-trips
-        // - Negative zero → "0"
-        // - NaN/Infinity → throw error
-    }
-
-    // JCS key comparison: UTF-16 code unit ordering
-    static int jcsCompare(String a, String b) {
-        // Compare as arrays of UTF-16 code units (unsigned)
-        int len = Math.min(a.length(), b.length());
-        for (int i = 0; i < len; i++) {
-            int diff = Character.compare(a.charAt(i), b.charAt(i));
-            if (diff != 0) return diff;
-        }
-        return Integer.compare(a.length(), b.length());
-    }
-}
-```
-
-Core implementation areas:
-
-**a) Property/Key Sorting:**
-Juneau already has `sortProperties()` which sorts bean properties 
alphabetically. For JCS, maps also need their keys sorted, and the sort must 
use UTF-16 code unit comparison (not locale-sensitive). Java's 
`String.compareTo()` already compares by UTF-16 code units, so `jcsCompare` can 
delegate to it for most cases. The key difference from the existing 
`sortProperties` is that JCS also requires sorting of **map keys** in 
`serializeMap`, which the parent `JsonSerializerSession` does not do [...]
-
-**b) ECMAScript Number Serialization:**
-This is the most complex part. Java's `Double.toString()` does NOT produce 
ECMAScript-compatible output. For example:
-- Java: `4.5E-7` → JCS requires: `4.5e-7` (lowercase `e`)
-- Java: `333333333.33333330` → JCS requires: `333333333.3333333` (no trailing 
zero)
-- Java: `-0.0` → JCS requires: `0`
-
-The implementation needs an ECMAScript-compatible number-to-string algorithm. 
Options:
-1. Adapt the Ryu algorithm (used by the JCS reference implementation) -- pure 
Java, no dependencies
-2. Use `Double.toString()` and post-process to match ECMAScript rules
-
-Post-processing `Double.toString()` is simpler and sufficient:
-- Remove trailing zeros after decimal point
-- Remove decimal point if no fraction remains
-- Lowercase `E` to `e`
-- Add `+` after `e` for positive exponents (ECMAScript does this)
-- Convert `-0.0` to `0`
-- Reject NaN and Infinity
-
-**c) String Serialization:**
-The existing JSON serializer already handles most string escaping. 
JCS-specific requirements:
-- Control characters U+0000-U+001F must use lowercase hex in `\uHHHH` (the 
existing serializer should be verified)
-- No unnecessary escaping of non-ASCII characters (they should be output as-is 
in UTF-8)
-- Lone surrogates must cause an error
-
-### 3. `Jcs.java` (Marshaller)
-
-Location: `src/main/java/org/apache/juneau/marshaller/Jcs.java`
-
-Extends `CharMarshaller`. Uses `JcsSerializer` for serialization and 
`JsonParser` for parsing (canonical JSON is valid JSON).
-
-```java
-package org.apache.juneau.marshaller;
-
-public class Jcs extends CharMarshaller {
-    public static final Jcs DEFAULT = new Jcs();
-
-    public static String of(Object object) throws SerializeException { ... }
-    public static <T> T to(String input, Class<T> type) throws ParseException 
{ ... }
-
-    public Jcs() { this(JcsSerializer.DEFAULT, JsonParser.DEFAULT); }
-    public Jcs(JcsSerializer s, JsonParser p) { super(s, p); }
-}
-```
-
-### 4. `package-info.java` updates
-
-Add JCS documentation to the existing `org.apache.juneau.json` package Javadoc.
-
-### 5. `annotation/JcsConfig.java`
-
-Annotation for specifying config properties for REST classes and methods. 
Empty initially (rank only), following the `MarkdownConfig`/`TomlConfig` 
pattern. JCS uses fixed canonical output, so no format-specific settings; the 
annotation provides consistency and future extensibility.
-
-```java
-@Target({ TYPE, METHOD })
-@Retention(RUNTIME)
-@Inherited
-@ContextApply(JcsConfigAnnotation.SerializerApply.class)
-public @interface JcsConfig {
-    int rank() default 0;
-}
-```
-
-Note: JcsConfig only has SerializerApply (no ParserApply) because JCS uses 
`JsonParser` for parsing, not a separate `JcsParser`.
-
-### 6. `annotation/JcsConfigAnnotation.java`
-
-Utility class for the `@JcsConfig` annotation, with no-op `SerializerApply` 
applier.
-
----
-
-## Files to Modify
-
-### 1. `BasicUniversalConfig.java`
-
-Add `JcsSerializer.class` to `serializers` (parser is already the standard 
JSON parser).
-
-### 2. `RestClient.java`
-
-Add `JcsSerializer.class` to the `universal()` method.
-
-### 3. Context: `Context.java`
-
-Path: 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java`
-
-Add `import org.apache.juneau.json.annotation.*;` (if not already present) and 
add `{@link JcsConfig}` to the list of config annotations in the 
`applyAnnotations()` Javadoc (alphabetically between JsonSchemaConfig and 
MarkdownConfig). Note: JcsConfig is in the `org.apache.juneau.json.annotation` 
package since JCS lives under the json package.
-
----
-
-## Test Plan
-
-### 1. Number Serialization Tests: 
`org/apache/juneau/json/JcsNumbers_Test.java`
-
-Tests against RFC 8785 Appendix B reference values.
-
-- **a01_zero** -- `0.0` → `0`
-- **a02_negativeZero** -- `-0.0` → `0`
-- **a03_minPosNumber** -- `5e-324`
-- **a04_minNegNumber** -- `-5e-324`
-- **a05_maxPosNumber** -- `1.7976931348623157e+308`
-- **a06_maxNegNumber** -- `-1.7976931348623157e+308`
-- **a07_maxPosInt** -- `9007199254740992`
-- **a08_maxNegInt** -- `-9007199254740992`
-- **a09_largeInteger** -- `295147905179352830000`
-- **a10_noTrailingZeros** -- `4.50` → `4.5`
-- **a11_scientificNotation** -- `1E30` → `1e+30`
-- **a12_smallDecimal** -- `2e-3` → `0.002`
-- **a13_verySmallDecimal** -- `0.000000000000000000000000001` → `1e-27`
-- **a14_rejectNaN** -- `Double.NaN` throws error
-- **a15_rejectInfinity** -- `Double.POSITIVE_INFINITY` throws error
-- **a16_rejectNegInfinity** -- `Double.NEGATIVE_INFINITY` throws error
-- **a17_integerNoDecimalPoint** -- `42.0` → `42`
-- **a18_roundToEven** -- `1424953923781206.2` (matches IEEE 754 round-to-even)
-- **a19_edgeCasePrecision** -- `333333333.3333333` (exact match)
-- **a20_floatValues** -- Float values promoted to double and serialized 
correctly
-
-### 2. String Serialization Tests: 
`org/apache/juneau/json/JcsStrings_Test.java`
-
-- **b01_simpleString** -- `"hello"` → `"hello"`
-- **b02_escapeBackslash** -- `\` → `"\\"`
-- **b03_escapeQuote** -- `"` → `"\""`
-- **b04_controlChars** -- `\b`, `\t`, `\n`, `\f`, `\r` use named escapes
-- **b05_otherControlChars** -- U+0000-U+001F (non-predefined) use lowercase 
`\u00XX`
-- **b06_nonAsciiLiteral** -- `€` (U+20AC) output as literal UTF-8 bytes, not 
`\u20ac`
-- **b07_rfcStringExample** -- `"€$\u000f\nA'B\"\\\\\"/"`matches RFC example
-- **b08_emojiLiteral** -- Emoji output as literal UTF-8 (surrogate pairs 
preserved)
-- **b09_loneSurrogateError** -- Lone surrogate (U+DEAD) causes error
-- **b10_emptyString** -- `""` → `""`
-
-### 3. Property Sorting Tests: `org/apache/juneau/json/JcsSorting_Test.java`
-
-- **c01_alphabeticalOrder** -- `{"a":1,"b":2,"c":3}` sorted
-- **c02_reverseInputOrder** -- `{"c":3,"b":2,"a":1}` → `{"a":1,"b":2,"c":3}`
-- **c03_nestedObjectsSorted** -- Inner objects also sorted recursively
-- **c04_arrayOrderPreserved** -- Array elements NOT reordered
-- **c05_utf16SortOrder** -- RFC sort test: `\r` < `1` < `\u0080` < `ö` < `€` < 
emoji < Hebrew
-- **c06_mapKeysSorted** -- `Map<String,Object>` keys sorted by UTF-16 code 
units
-- **c07_emptyObject** -- `{}` → `{}`
-- **c08_singleProperty** -- `{"a":1}` → `{"a":1}`
-- **c09_numericStringKeys** -- `"1"` < `"10"` < `"2"` (lexicographic, not 
numeric)
-- **c10_caseSensitive** -- `"A"` < `"a"` (uppercase before lowercase in UTF-16)
-
-### 4. Full Canonicalization Tests: 
`org/apache/juneau/json/JcsCanonical_Test.java`
-
-End-to-end tests matching RFC 8785 examples.
-
-- **d01_rfcExample1** -- RFC Section 3.2.2 full example (numbers + string + 
literals)
-- **d02_rfcExample2** -- RFC Section 3.2.3 sorted version
-- **d03_rfcByteOutput** -- RFC Section 3.2.4 exact UTF-8 byte sequence
-- **d04_noWhitespace** -- No spaces, tabs, newlines in output
-- **d05_simpleBeanCanonical** -- Bean with multiple properties, deterministic 
output
-- **d06_nestedBeanCanonical** -- Nested beans, all levels sorted
-- **d07_mixedTypesCanonical** -- Bean with strings, numbers, booleans, nulls, 
arrays, nested objects
-- **d08_deterministicRoundTrip** -- Serialize same bean twice, byte-identical 
output
-- **d09_hashStability** -- SHA-256 of output is identical across multiple 
serializations
-- **d10_rfcSortingTestData** -- RFC sorting test with Unicode keys (exact 
match)
-
-### 5. Marshaller Tests: `org/apache/juneau/marshaller/Jcs_Test.java`
-
-- **e01_of** -- `Jcs.of(bean)` produces canonical JSON string
-- **e02_to** -- `Jcs.to(input, Type)` parses using standard JSON parser
-- **e03_roundTrip** -- Serialize + parse round-trip
-- **e04_defaultInstance** -- `Jcs.DEFAULT` works correctly
-
-### 6. Edge Case Tests: `org/apache/juneau/json/JcsEdgeCases_Test.java`
-
-- **f01_emptyBean** -- `{}`
-- **f02_nullRoot** -- `null`
-- **f03_booleanRoot** -- `true` / `false`
-- **f04_numberRoot** -- `42`
-- **f05_stringRoot** -- `"hello"`
-- **f06_arrayRoot** -- `[1,2,3]`
-- **f07_deepNesting** -- 10+ levels, all sorted
-- **f08_duplicateMapKeys** -- Last value wins (JSON semantics), sorted output
-- **f09_largeObject** -- 100+ properties, correctly sorted
-- **f10_emptyStrings** -- Empty string keys and values handled
-
----
-
-### 7. Cross-Format Round-Trip Integration
-
-JCS output is valid JSON, so `JsonParser` can parse it. Use **full round-trip 
entries** (no `.returnOriginalObject()`) in all TESTERS arrays. The entry 
pattern is:
-
-```java
-tester(N, "Jcs - default")
-    
.serializer(JcsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
-    .parser(JsonParser.create())
-    .build(),
-```
-
-Add this entry to **all** of the following TESTERS arrays (assign the next 
available index `N` in each file):
-
-- `RoundTripTest_Base.java` — base class; all subclass tests inherit its 
TESTERS list
-- `RoundTripDateTime_Test.java` — verifies date/time and Duration values 
serialize correctly in JCS output
-- Any other files with their own private `TESTERS` arrays (e.g., 
`RoundTripMaps_Test`, `RoundTripBeanMaps_Test`)
-
-**Note**: Certain types have spec-level constraints (see Limitations section). 
The following `skipIf` predicates may be needed for values that JCS cannot 
represent:
-
-```java
-tester(N, "Jcs - default")
-    
.serializer(JcsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
-    .parser(JsonParser.create())
-    .skipIf(o -> o instanceof Double && (((Double)o).isNaN() || 
((Double)o).isInfinite()))
-    .build(),
-```
-
-## Documentation
-
-### 1. Javadoc
-
-Follow the exact structure of `JsonSerializer.java` for the serializer and 
`Json.java` for the marshaller.
-
-**`JcsSerializer.java`** — full class-level Javadoc structure:
-
-```
-/**
- * Serializes POJO models to canonical JSON per RFC 8785 (JCS).
- *
- * <h5 class='topic'>Media types</h5>
- * <p>
- *     Produces media type: <bc>application/json</bc>
- * </p>
- *
- * <h5 class='topic'>Description</h5>
- * <p>
- *     JCS (JSON Canonicalization Scheme) produces a deterministic, 
byte-for-byte canonical
- *     representation of JSON, enabling reliable cryptographic operations such 
as hashing and
- *     digital signing. All canonicalization rules are defined in
- *     <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 
8785</a>.
- * </p>
- * <ul class='spaced-list'>
- *     <li>No whitespace between tokens.
- *     <li>Object properties sorted by UTF-16 code unit order, applied 
recursively to all nested objects.
- *     <li>Numbers serialized using ECMAScript-compatible rules: shortest 
round-trip representation,
- *         no trailing zeros, lowercase {@code e}, positive exponent sign 
included (e.g. {@code 1e+30}).
- *     <li>Negative zero serialized as {@code 0}.
- *     <li>Non-ASCII characters emitted as literal UTF-8 bytes (no unnecessary 
{@code \uHHHH} escaping).
- *     <li>Array element order is preserved (not sorted).
- * </ul>
- *
- * <h5 class='section'>Example:</h5>
- * <p class='bjava'>
- *     <jc>// Create a bean.</jc>
- *     MyBean <jv>bean</jv> = <jk>new</jk> 
MyBean().name(<js>"Alice"</js>).age(30);
- *
- *     <jc>// Serialize to canonical JSON.</jc>
- *     String <jv>json</jv> = 
JcsSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>bean</jv>);
- *
- *     <jc>// Or use the Jcs marshaller convenience method.</jc>
- *     String <jv>json</jv> = Jcs.<jsm>of</jsm>(<jv>bean</jv>);
- * </p>
- *
- * <h5 class='figure'>Example output (bean with name/age):</h5>
- * <p class='bjson'>
- *     {<jok>"age"</jok>:<jov>30</jov>,<jok>"name"</jok>:<jov>"Alice"</jov>}
- * </p>
- *
- * <h5 class='figure'>Example output with nested object:</h5>
- * <p class='bjson'>
- *     
{<jok>"address"</jok>:{<jok>"city"</jok>:<jov>"Denver"</jov>,<jok>"zip"</jok>:<jov>"80201"</jov>},<jok>"name"</jok>:<jov>"Alice"</jov>}
- * </p>
- *
- * <h5 class='section'>Notes:</h5><ul>
- *     <li class='note'>This class is thread safe and reusable.
- *     <li class='note'>
- *             JCS output is valid JSON and can be parsed using the standard 
{@link JsonParser}.
- *             The {@link Jcs} marshaller pairs this serializer with {@link 
JsonParser} for full round-trip support.
- *     <li class='note'>
- *             {@link java.math.BigDecimal} and {@link java.math.BigInteger} 
values beyond IEEE 754 double
- *             precision range will lose precision or throw a {@link 
SerializeException}.
- *             This is a spec-level constraint defined by RFC 8785.
- *     <li class='note'>
- *             {@link Double#NaN}, {@link Double#POSITIVE_INFINITY}, and 
{@link Double#NEGATIVE_INFINITY}
- *             are not permitted and will throw a {@link SerializeException}.
- *     <li class='note'>
- *             Strings containing lone UTF-16 surrogate code units will throw 
a {@link SerializeException}.
- *     <li class='note'>
- *             JCS does not define its own media type. Output is {@code 
application/json}.
- *             Do not register this serializer as a content-negotiation 
alternative to {@link JsonSerializer};
- *             use it explicitly by class reference.
- * </ul>
- *
- * <h5 class='section'>See Also:</h5><ul>
- *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 8785 — JSON Canonicalization 
Scheme</a>
- *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/Jcs";>JCS topic</a>
- *     <li class='jc'>{@link Jcs}
- *     <li class='jc'>{@link JsonParser}
- * </ul>
- */
-```
-
-**`Jcs.java`** (marshaller) — full class-level Javadoc structure:
-
-```
-/**
- * A pairing of a {@link JcsSerializer} and {@link JsonParser} into a single 
class with
- * convenience read/write methods.
- *
- * <p>
- *     Produces canonical JSON per <a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 8785</a>.
- *     Parsing uses the standard {@link JsonParser} since JCS output is valid 
JSON.
- * </p>
- *
- * <h5 class='figure'>Examples:</h5>
- * <p class='bjava'>
- *     <jc>// Using static convenience methods.</jc>
- *     String <jv>s</jv> = Jcs.<jsm>of</jsm>(<jv>myBean</jv>);
- *     MyBean <jv>b</jv> = Jcs.<jsm>to</jsm>(<jv>s</jv>, 
MyBean.<jk>class</jk>);
- * </p>
- * <p class='bjava'>
- *     <jc>// Using instance.</jc>
- *     Jcs <jv>jcs</jv> = <jk>new</jk> Jcs();
- *     String <jv>s</jv> = <jv>jcs</jv>.write(<jv>myBean</jv>);
- *     MyBean <jv>b</jv> = <jv>jcs</jv>.read(<jv>s</jv>, 
MyBean.<jk>class</jk>);
- * </p>
- *
- * <h5 class='figure'>Example output (bean with name/age):</h5>
- * <p class='bjson'>
- *     {<jok>"age"</jok>:<jov>30</jov>,<jok>"name"</jok>:<jov>"Alice"</jov>}
- * </p>
- *
- * <h5 class='section'>Notes:</h5><ul>
- *     <li class='note'>
- *             {@link java.math.BigDecimal} and {@link java.math.BigInteger} 
values beyond IEEE 754 double
- *             precision range will lose precision or throw during 
serialization.
- *     <li class='note'>
- *             {@link Double#NaN}, {@link Double#POSITIVE_INFINITY}, and 
{@link Double#NEGATIVE_INFINITY}
- *             are not permitted and will throw during serialization.
- * </ul>
- *
- * <h5 class='section'>See Also:</h5><ul>
- *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8785";>RFC 8785 — JSON Canonicalization 
Scheme</a>
- *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/Jcs";>JCS topic</a>
- *     <li class='jc'>{@link JcsSerializer}
- *     <li class='jc'>{@link JsonParser}
- * </ul>
- */
-```
-
-**No separate `JcsParser` class** — parsing is handled by the existing `{@link 
JsonParser}`.
-
-### 2. Package Javadoc
-
-Add JCS section to existing `org.apache.juneau.json` package documentation.
-
-### 3. Update Release Notes
-/Users/james.bognar/git/apache/juneau/docs/pages/release-notes
-
-### 4. Update Documentation
-
-Find how existing languages are documented in the following location and 
update the documentation to match
-the same level of detail.
-/Users/james.bognar/git/apache/juneau/docs/pages/topics
-
----
-
-## Implementation Order
-
-1. **ECMAScript number serialization** -- Implement and unit test the number 
formatting algorithm
-2. **`JcsSerializer.java`** and **`JcsSerializerSession.java`** -- Extend 
JsonSerializer with sorting and canonical output
-3. **Number tests** (`JcsNumbers_Test.java`) -- 20 test cases against RFC 
reference values
-4. **String tests** (`JcsStrings_Test.java`) -- 10 test cases
-5. **Sorting tests** (`JcsSorting_Test.java`) -- 10 test cases
-6. **Canonical tests** (`JcsCanonical_Test.java`) -- 10 test cases with RFC 
byte-level verification
-7. **`Jcs.java`** marshaller
-8. **Marshaller and edge case tests** -- 14 test cases
-9. **REST integration** (`BasicUniversalConfig`, `RestClient`)
-10. **Context registration** (`JcsConfig`, `JcsConfigAnnotation`, 
`Context.java`)
-11. **Documentation**
-
----
-
-## Iterator/Iterable/Stream Support
-
-As of 9.2.1, Juneau natively supports serialization of `Iterator`, `Iterable` 
(non-Collection), `Enumeration`, and `java.util.stream.Stream` types.
-
-This format **inherits** Iterator/Iterable/Stream support from its parent 
`JsonSerializerSession`. The `serializeStreamable()` method in the base class 
handles lazy streaming of these types. Verify that any overrides of 
`serializeAnything()` in this serializer session do not bypass the base class 
handling of `isStreamable()` types.
-
----
-
-## Key Design Decisions
-
-### 1. Full round-trip via JsonParser (no dedicated parser)
-
-JCS output is valid JSON. The `Jcs` marshaller pairs `JcsSerializer` with the 
standard `JsonParser`, enabling full round-trip serialization and 
deserialization at the same level as JSON. No separate `JcsParser` class is 
needed or created.
-
-### 2. Extends JsonSerializer
-
-JCS is a strict subset of JSON output. It builds directly on `JsonSerializer`, 
overriding only the methods that need canonical behavior: property sorting, 
number formatting, and string escaping. This minimizes code duplication.
-
-### 3. UTF-16 code unit sorting
-
-JCS mandates UTF-16 code unit comparison for property sorting. Java's 
`String.compareTo()` already performs this comparison (Java strings are UTF-16 
internally), so no conversion is needed.
-
-### 4. ECMAScript number formatting via post-processing
-
-Rather than implementing the full Ryu algorithm, the implementation 
post-processes `Double.toString()` output to match ECMAScript's 
`JSON.stringify()`. This is simpler and sufficient because:
-- Java's `Double.toString()` already produces a round-trip-safe representation
-- The post-processing rules are straightforward (lowercase `e`, remove 
trailing zeros, handle `-0.0`)
-
-### 5. No separate media type
-
-JCS output is standard `application/json`. It does not define its own media 
type. The serializer produces `application/json` with `+jcs` as an optional 
qualifier.
-
-### 6. Pre-configured sortProperties()
-
-The builder calls `sortProperties()` in the constructor, ensuring bean 
properties are sorted. Additionally, `serializeMap()` is overridden to sort map 
keys, which `sortProperties()` alone does not cover.
-
-### 7. Date/Time and Duration Support
-
-Date/time types (`Date`, `Calendar`, `Temporal` subtypes) and `Duration` are 
built-in first-class types. JCS extends `JsonSerializer`, so it inherits the 
`isDateOrCalendarOrTemporal()` and `isDuration()` dispatch checks from 
`JsonSerializerSession`. These types are formatted as ISO 8601 strings; the 
deterministic property ordering applies to the string representation. See 
`builtin-datetime-iso8601.md` for the full architecture.
-
----
-
-## Scope Exclusions
-
-- **Dedicated JcsParser class** -- No separate parser is needed; JCS output is 
valid JSON and `JsonParser` handles it directly
-- **Signature verification** -- JCS defines the canonical form; signature 
schemes are application-level concerns
-- **Unicode normalization** -- RFC 8785 explicitly excludes Unicode 
normalization
-- **Extended precision numbers** -- Numbers must fit in IEEE 754 double 
precision per the spec; `BigDecimal`/`BigInteger` values beyond this range are 
not supported

Reply via email to