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