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 309531fa84 feat(marshall): JSON5L (JSON5 + JSONL) line-delimited format
309531fa84 is described below
commit 309531fa8470e0d3a2370c1cfbc387e90c3806e6
Author: James Bognar <[email protected]>
AuthorDate: Wed Jun 17 08:52:31 2026 -0400
feat(marshall): JSON5L (JSON5 + JSONL) line-delimited format
---
.../juneau/marshall/json5l/Json5lConfig.java | 47 +++++
.../marshall/json5l/Json5lConfigAnnotation.java | 76 +++++++
.../juneau/marshall/json5l/Json5lParser.java | 154 ++++++++++++++
.../marshall/json5l/Json5lParserSession.java | 197 ++++++++++++++++++
.../juneau/marshall/json5l/Json5lSerializer.java | 200 ++++++++++++++++++
.../marshall/json5l/Json5lSerializerSession.java | 139 +++++++++++++
.../juneau/marshall/json5l/Json5lTokenReader.java | 78 +++++++
.../juneau/marshall/json5l/package-info.java | 45 ++++
.../apache/juneau/marshall/marshaller/Json5l.java | 229 +++++++++++++++++++++
.../org/apache/juneau/ComboRoundTripTest_Base.java | 28 +++
.../org/apache/juneau/ComboRoundTrip_Tester.java | 4 +
.../marshall/json5l/Json5lCoverage_Test.java | 199 ++++++++++++++++++
.../marshall/json5l/Json5lMediaType_Test.java | 73 +++++++
.../juneau/marshall/json5l/Json5lParser_Test.java | 155 ++++++++++++++
.../marshall/json5l/Json5lRoundTrip_Test.java | 78 +++++++
.../marshall/json5l/Json5lTokenStream_Test.java | 136 ++++++++++++
.../apache/juneau/marshall/json5l/Json5l_Test.java | 182 ++++++++++++++++
.../org/apache/juneau/marshaller/Json5l_Test.java | 99 +++++++++
.../org/apache/juneau/ComboRoundTripTest_Base.java | 28 +++
.../org/apache/juneau/ComboRoundTrip_Tester.java | 4 +
.../juneau/rest/client/classic/RestClient.java | 3 +
.../org/apache/juneau/http/header/ContentType.java | 2 +
.../juneau/http/header/ContentType_Test.java | 1 +
.../server/reactive/ReactiveResponseProcessor.java | 4 +-
.../rest/server/config/BasicUniversalConfig.java | 5 +
25 files changed, 2164 insertions(+), 2 deletions(-)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lConfig.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lConfig.java
new file mode 100644
index 0000000000..d3efc265e4
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lConfig.java
@@ -0,0 +1,47 @@
+/*
+ * 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.marshall.json5l;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.marshall.*;
+
+/**
+ * Annotation for specifying config properties for REST classes and methods
using JSON5L.
+ *
+ * <p>
+ * Used primarily for specifying bean configuration properties on REST classes
and methods.
+ */
+@Target({ TYPE, METHOD })
+@Retention(RUNTIME)
+@Inherited
+@ContextApply({ Json5lConfigAnnotation.SerializerApply.class,
Json5lConfigAnnotation.ParserApply.class })
+public @interface Json5lConfig {
+
+ /**
+ * 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/marshall/json5l/Json5lConfigAnnotation.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lConfigAnnotation.java
new file mode 100644
index 0000000000..d9623304ff
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lConfigAnnotation.java
@@ -0,0 +1,76 @@
+/*
+ * 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.marshall.json5l;
+
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.commons.svl.*;
+import org.apache.juneau.marshall.*;
+import org.apache.juneau.marshall.json.*;
+
+/**
+ * Utility classes and methods for the {@link Json5lConfig @Json5lConfig}
annotation.
+ */
+public class Json5lConfigAnnotation {
+
+ private Json5lConfigAnnotation() {}
+
+ /**
+ * Applies {@link Json5lConfig} annotations to a {@link
org.apache.juneau.marshall.json.JsonParser.Builder}.
+ */
+ @SuppressWarnings({
+ "rawtypes" // Raw types required for reflective annotation
application.
+ })
+ public static class ParserApply extends AnnotationApplier<Json5lConfig,
JsonParser.Builder> {
+
+ /**
+ * Constructor.
+ *
+ * @param vr The resolver for resolving values in annotations.
+ */
+ public ParserApply(VarResolverSession vr) {
+ super(Json5lConfig.class, JsonParser.Builder.class, vr);
+ }
+
+ @Override
+ public void apply(AnnotationInfo<Json5lConfig> ai,
JsonParser.Builder b) {
+ // No-op: JSON5L reuses JsonParser.Builder; no
format-specific settings initially
+ }
+ }
+
+ /**
+ * Applies {@link Json5lConfig} annotations to a {@link
org.apache.juneau.marshall.json.JsonSerializer.Builder}.
+ */
+ @SuppressWarnings({
+ "rawtypes" // Raw types required for reflective annotation
application.
+ })
+ public static class SerializerApply extends
AnnotationApplier<Json5lConfig, JsonSerializer.Builder> {
+
+ /**
+ * Constructor.
+ *
+ * @param vr The resolver for resolving values in annotations.
+ */
+ public SerializerApply(VarResolverSession vr) {
+ super(Json5lConfig.class, JsonSerializer.Builder.class,
vr);
+ }
+
+ @Override
+ public void apply(AnnotationInfo<Json5lConfig> ai,
JsonSerializer.Builder b) {
+ // No-op: JSON5L reuses JsonSerializer.Builder; no
format-specific settings initially
+ }
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lParser.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lParser.java
new file mode 100644
index 0000000000..51b6c0c620
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lParser.java
@@ -0,0 +1,154 @@
+/*
+ * 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.marshall.json5l;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.marshall.json5.*;
+
+/**
+ * Parses JSON5L (JSON5 Lines) input into POJO models.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Handles <c>Content-Type</c> types: <bc>application/json5l, text/json5l</bc>
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * JSON5L combines the relaxed JSON5 dialect with JSONL's newline-delimited
framing. Each non-empty,
+ * non-comment line is parsed as a complete JSON5 value (single-quoted
strings, unquoted field names,
+ * trailing commas, comments, relaxed numbers). Because JSON5 is a strict
superset of JSON, this
+ * parser also reads plain JSONL input unchanged.
+ *
+ * <p>
+ * Comment handling follows JSON5 line-by-line: a line that is blank or
contains only a comment is
+ * skipped (like a blank line); inline comments within a record's JSON are
tolerated. A
+ * <c>/* */</c> block comment must open and close on the same physical
line — block comments
+ * cannot span record boundaries.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Parse JSON5L into a list of POJOs</jc>
+ * List<MyBean> <jv>list</jv> =
Json5lParser.<jsf>DEFAULT</jsf>.parse(<jv>json5lInput</jv>,
List.<jk>class</jk>, MyBean.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='figure'>Example input:</h5>
+ * <p class='bjson'>
+ * // header comment line
+ * {name:'Alice',age:30}
+ * {name:'Bob',age:25} // trailing comment
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>This class is thread safe and reusable.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/Json5lBasics">JSON5L Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S110", // Inheritance depth acceptable for Json5lParser hierarchy
+ "java:S115" // Constants use naming conventions that embed type info
or config keys
+})
+public class Json5lParser extends Json5Parser {
+
+ private static final String ARG_copyFrom = "copyFrom";
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends Json5Parser.Builder {
+
+ private static final Cache<HashKey,Json5lParser> CACHE =
Cache.of(HashKey.class, Json5lParser.class).build();
+
+ /**
+ * Constructor, default settings.
+ */
+ protected Builder() {
+
consumes("application/json5l,text/json5l,application/jsonl,application/x-ndjson,text/jsonl")
+ .type(Json5lParser.class);
+ }
+
+ /**
+ * 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 bean to copy from.
+ * <br>Cannot be <jk>null</jk>.
+ */
+ protected Builder(Json5lParser copyFrom) {
+ super(assertArgNotNull(ARG_copyFrom, copyFrom));
+ }
+
+ @Override /* Overridden from Context.Builder<?> */
+ public Json5lParser build() {
+ return cache(CACHE).build(Json5lParser.class);
+ }
+
+ @Override /* Overridden from Context.Builder<?> */
+ public Builder copy() {
+ return new Builder(this);
+ }
+ }
+
+ /** Default parser, Accept=application/json5l. */
+ public static final Json5lParser DEFAULT = new Json5lParser(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 Json5lParser(Builder builder) {
+ super(builder);
+ }
+
+ @Override /* Overridden from Context */
+ public Builder copy() {
+ return new Builder(this);
+ }
+
+ @Override /* Overridden from Context */
+ public Json5lParserSession.Builder createSession() {
+ return Json5lParserSession.create(this);
+ }
+
+ @Override /* Overridden from Context */
+ public Json5lParserSession getSession() {
+ return createSession().build();
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lParserSession.java
new file mode 100644
index 0000000000..eb26c47228
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lParserSession.java
@@ -0,0 +1,197 @@
+/*
+ * 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.marshall.json5l;
+
+import java.io.*;
+
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.marshall.*;
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.json5.*;
+import org.apache.juneau.marshall.parser.*;
+import org.apache.juneau.marshall.stream.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link
Json5lParser}.
+ *
+ * <p>
+ * Extends {@link Json5ParserSession} to inherit the full JSON5 dialect
(single-quoted strings,
+ * bare/unquoted field names, trailing commas, missing values, comments,
relaxed numbers) and
+ * re-adds JSONL's newline-delimited framing: input is read one line at a
time, blank and
+ * comment-only lines are skipped, and each remaining line is parsed as one
JSON5 value.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='warn'>This class is not thread safe and is typically
discarded after one use.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/Json5lBasics">JSON5L Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "unchecked", // Type erasure: elementType is ClassMeta<?>;
toArray/convertToType return Object
+ "java:S110", // Inheritance depth acceptable
+ "resource" // Closeable resources are owned by the caller's parser
session; Eclipse JDT @Owning warning is by design.
+})
+public class Json5lParserSession extends Json5ParserSession {
+
+ /**
+ * Builder class.
+ */
+ @SuppressWarnings({
+ "java:S110" // Inheritance depth is intentional across parser
session builders
+ })
+ public static class Builder extends Json5ParserSession.Builder {
+
+ /**
+ * Constructor
+ *
+ * @param ctx The context creating this session.
+ */
+ protected Builder(Json5lParser ctx) {
+ super(ctx);
+ }
+
+ @Override
+ public Json5lParserSession build() {
+ return new Json5lParserSession(this);
+ }
+ }
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @param ctx The context creating this session.
+ * @return A new builder.
+ */
+ public static Builder create(Json5lParser ctx) {
+ return new Builder(ctx);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ protected Json5lParserSession(Builder builder) {
+ super(builder);
+ }
+
+ /**
+ * Opens a low-level pull-parser cursor over a JSON5L document, bound
to this live session.
+ *
+ * <p>
+ * Each top-level JSON5 value (one per line) is emitted as a flat
sequence at depth 0; there is
+ * no virtual root container. Same honored/ignored builder properties
as
+ * {@link Json5ParserSession#parseTokens(Object)}.
+ *
+ * @param input The input. Accepts {@link Reader}, {@link
CharSequence}, {@link InputStream},
+ * <code><jk>byte</jk>[]</code>, or {@link File}.
+ * @return A new {@link Json5lTokenReader}.
+ * @throws IOException If a problem occurred opening the underlying
input.
+ */
+ @SuppressWarnings({
+ "java:S2095" // ParserPipe lifecycle is transferred to the
returned Json5lTokenReader, which closes it via its own close(); the caller
owns the cursor via try-with-resources.
+ })
+ @Override /* Json5ParserSession */
+ public TokenReader parseTokens(Object input) throws IOException {
+ var pipe = new ParserPipe(
+ input,
+ isDebug(),
+ false /* strict */,
+ isAutoCloseStreams(),
+ isUnbuffered(),
+ getStreamCharset(),
+ getFileCharset());
+ return new Json5lTokenReader(pipe, new
JsonTokenReader.Settings(isTrimStrings()), this);
+ }
+
+ /**
+ * Streaming array-element {@link RecordReader} for JSON5L.
+ *
+ * <p>
+ * Aliases the JSON5L line record stream: the {@link Json5lTokenReader}
returned by
+ * {@link #parseTokens(Object)} is itself a {@link RecordReader} that
yields one record per
+ * top-level line at depth 0. Memory is O(1) in the number of lines.
+ *
+ * @param input The input.
+ * @return A new line-delimited {@link RecordReader}.
+ * @throws IOException If a problem occurred opening the underlying
input.
+ */
+ @Override /* ArrayRecordReadable */
+ public RecordReader parseArrayRecords(Object input) throws IOException {
+ return parseTokens(input);
+ }
+
+ @Override /* Overridden from JsonParserSession */
+ protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws
IOException, ParseException, ExecutableException {
+ try (var r = pipe.getParserReader()) {
+ if (r == null)
+ return null;
+
+ if (type.isCollectionOrArray()) {
+ var elementType = type.getElementType();
+ var results = newGenericList();
+ var br = new BufferedReader(r);
+ String line;
+ while ((line = br.readLine()) != null) {
+ var trimmed = line.trim();
+ if (isParseable(trimmed)) {
+ try (var linePipe =
createPipe(trimmed)) {
+ var item =
super.doParse(linePipe, elementType);
+ results.add(item);
+ }
+ }
+ }
+ return type.isArray() ? (T) toArray(type,
results) : (T) convertToType(results, type);
+ }
+
+ // Single object: parse just the first non-empty,
non-comment line
+ var br = new BufferedReader(r);
+ String line;
+ while ((line = br.readLine()) != null) {
+ var trimmed = line.trim();
+ if (isParseable(trimmed)) {
+ try (var linePipe =
createPipe(trimmed)) {
+ return super.doParse(linePipe,
type);
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Returns <jk>true</jk> if the trimmed line contains a JSON5 value to
parse — i.e. it is neither
+ * blank nor a comment-only line (a line consisting solely of a
<c>//</c> line comment or a
+ * single-line <c>/* */</c> block comment).
+ *
+ * @param trimmed The whitespace-trimmed line.
+ * @return <jk>true</jk> if the line should be parsed as a JSON5 value.
+ */
+ private static boolean isParseable(String trimmed) {
+ if (trimmed.isEmpty())
+ return false;
+ if (trimmed.startsWith("//"))
+ return false;
+ // A line that is exactly a single block comment (possibly with
surrounding whitespace,
+ // already trimmed) is comment-only. Anything after the
closing */ makes it parseable.
+ if (trimmed.startsWith("/*") && trimmed.endsWith("*/") &&
trimmed.indexOf("*/") == trimmed.length() - 2)
+ return false;
+ return true;
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lSerializer.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lSerializer.java
new file mode 100644
index 0000000000..17bc08015f
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lSerializer.java
@@ -0,0 +1,200 @@
+/*
+ * 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.marshall.json5l;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.jsonl.*;
+
+/**
+ * Serializes POJO models to JSON5L (JSON5 Lines).
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Handles <c>Accept</c> types: <bc>application/json5l, text/json5l</bc>
+ * <p>
+ * Produces <c>Content-Type</c> types: <bc>application/json5l</bc>
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * JSON5L combines the relaxed JSON5 dialect with JSONL's newline-delimited
framing: each top-level
+ * value is written as a compact document on its own line, exactly as {@link
JsonlSerializer} does.
+ * <p>
+ * By <b>default</b> the per-line output is strict RFC-8259 JSON
(byte-identical to
+ * {@link JsonlSerializer}), keeping output maximally portable. The {@link
Builder#json5Sugar()
+ * json5Sugar()} opt-in switches the per-line output to JSON5 sugar
(single-quoted strings, unquoted
+ * field names where safe).
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Strict-per-line output (default)</jc>
+ * String <jv>json5l</jv> =
Json5lSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>myList</jv>);
+ *
+ * <jc>// JSON5-sugar-per-line output</jc>
+ * Json5lSerializer <jv>serializer</jv> =
Json5lSerializer.<jsm>create</jsm>().json5Sugar().build();
+ * String <jv>json5l</jv> = <jv>serializer</jv>.serialize(<jv>myList</jv>);
+ * </p>
+ *
+ * <h5 class='figure'>Example output, sugar off (List of beans):</h5>
+ * <p class='bjson'>
+ * {"name":"Alice","age":30}
+ * {"name":"Bob","age":25}
+ * </p>
+ *
+ * <h5 class='figure'>Example output, sugar on:</h5>
+ * <p class='bjson'>
+ * {name:'Alice',age:30}
+ * {name:'Bob',age:25}
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>This class is thread safe and reusable.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/Json5lBasics">JSON5L Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S110" // Inheritance depth acceptable
+})
+public class Json5lSerializer extends JsonlSerializer {
+
+ private static final String ARG_copyFrom = "copyFrom";
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends JsonSerializer.Builder<Builder> {
+
+ private static final Cache<HashKey,Json5lSerializer> CACHE =
Cache.of(HashKey.class, Json5lSerializer.class).build();
+
+ boolean json5Sugar;
+
+ /**
+ * Constructor, default settings.
+ */
+ protected Builder() {
+ produces("application/json5l")
+
.accept("application/json5l,text/json5l,application/jsonl;q=0.9,application/x-ndjson;q=0.9,text/jsonl;q=0.9")
+ .type(Json5lSerializer.class)
+ .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));
+ json5Sugar = copyFrom.json5Sugar;
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param copyFrom The bean to copy from.
+ * <br>Cannot be <jk>null</jk>.
+ */
+ protected Builder(Json5lSerializer copyFrom) {
+ super(assertArgNotNull(ARG_copyFrom, copyFrom));
+ json5Sugar = copyFrom.json5Sugar;
+ }
+
+ /**
+ * Emit JSON5 sugar (single-quoted strings, unquoted field
names where safe) on each line
+ * instead of strict RFC-8259 JSON.
+ *
+ * <p>
+ * Off by default, in which case the per-line output is
byte-identical to
+ * {@link JsonlSerializer}.
+ *
+ * @return This object.
+ */
+ public Builder json5Sugar() {
+ json5Sugar = true;
+ return this;
+ }
+
+ @Override /* Overridden from Context.Builder<?> */
+ public HashKey hashKey() {
+ return HashKey.of(super.hashKey(), json5Sugar);
+ }
+
+ @Override /* Overridden from Context.Builder<?> */
+ public Json5lSerializer build() {
+ return cache(CACHE).build(Json5lSerializer.class);
+ }
+
+ @Override /* Overridden from Context.Builder<?> */
+ public Builder copy() {
+ return new Builder(this);
+ }
+ }
+
+ /** Default serializer, strict-JSON-per-line. */
+ public static final Json5lSerializer DEFAULT = new
Json5lSerializer(create());
+
+ final boolean json5Sugar;
+
+ /**
+ * 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 Json5lSerializer(Builder builder) {
+ super(builder.useWhitespace(false));
+ json5Sugar = builder.json5Sugar;
+ }
+
+ /**
+ * Returns <jk>true</jk> if this serializer emits JSON5 sugar on each
line.
+ *
+ * @return <jk>true</jk> if JSON5 sugar is enabled.
+ */
+ public boolean isJson5Sugar() {
+ return json5Sugar;
+ }
+
+ @Override /* Overridden from Context */
+ public Builder copy() {
+ return new Builder(this);
+ }
+
+ @Override /* Overridden from Context */
+ public Json5lSerializerSession.Builder createSession() {
+ return Json5lSerializerSession.create(this);
+ }
+
+ @Override /* Overridden from Context */
+ public Json5lSerializerSession getSession() {
+ return createSession().build();
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lSerializerSession.java
new file mode 100644
index 0000000000..91fc7ee791
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lSerializerSession.java
@@ -0,0 +1,139 @@
+/*
+ * 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.marshall.json5l;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.jsonl.*;
+import org.apache.juneau.marshall.serializer.*;
+import org.apache.juneau.marshall.stream.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link
Json5lSerializer}.
+ *
+ * <p>
+ * Extends {@link JsonlSerializerSession} to inherit JSONL's line-delimited
framing. When the
+ * owning {@link Json5lSerializer} has {@link
Json5lSerializer.Builder#json5Sugar() json5Sugar}
+ * enabled, the per-line output switches to JSON5 sugar (single-quoted
strings, unquoted field names
+ * where safe) on both the databind and token-streaming paths; otherwise the
inherited strict
+ * RFC-8259 output is used unchanged.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='warn'>This class is not thread safe and is typically
discarded after one use.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/Json5lBasics">JSON5L Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "resource", // Resource management handled externally
+ "java:S110", // Inheritance depth acceptable
+ "java:S115" // Constants use UPPER_snakeCase convention
+})
+public class Json5lSerializerSession extends JsonlSerializerSession {
+
+ private static final String ARG_ctx = "ctx";
+
+ private static final char SUGAR_QUOTE = '\'';
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends JsonlSerializerSession.Builder {
+
+ final Json5lSerializer ctx;
+
+ /**
+ * Constructor
+ *
+ * @param ctx The context creating this session.
+ * <br>Cannot be <jk>null</jk>.
+ */
+ protected Builder(Json5lSerializer ctx) {
+ super(assertArgNotNull(ARG_ctx, ctx));
+ this.ctx = ctx;
+ }
+
+ @Override
+ public Json5lSerializerSession build() {
+ return new Json5lSerializerSession(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(Json5lSerializer ctx) {
+ return new Builder(assertArgNotNull(ARG_ctx, ctx));
+ }
+
+ private final boolean json5Sugar;
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ protected Json5lSerializerSession(Builder builder) {
+ super(builder);
+ json5Sugar = builder.ctx.isJson5Sugar();
+ }
+
+ @Override /* Overridden from JsonSerializerSession */
+ protected JsonWriter getJsonWriter(SerializerPipe out) {
+ if (! json5Sugar)
+ return super.getJsonWriter(out);
+ var output = out.getRawOutput();
+ if (output instanceof JsonWriter output2)
+ return output2;
+ var w = JsonWriter.create(out.getWriter(), isUseWhitespace(),
getMaxIndent(), isEscapeSolidus(), SUGAR_QUOTE, true, isTrimStrings(),
getUriResolver());
+ out.setWriter(w);
+ return w;
+ }
+
+ @Override /* Overridden from JsonlSerializerSession */
+ public TokenWriter serializeTokens(Object output) throws IOException {
+ if (! json5Sugar)
+ return super.serializeTokens(output);
+ var walk = new PojoWalker.Options(
+ isKeepNullProperties(),
+ isTrimEmptyMaps(),
+ isTrimEmptyCollections(),
+ isSortMaps(),
+ isSortCollections(),
+ isTrimStrings(),
+ getMarshallingContext());
+ var settings = new JsonTokenWriter.Settings(
+ false /* useWhitespace — JSON5L is one-record-per-line,
no pretty-print */,
+ getMaxIndent(),
+ SUGAR_QUOTE,
+ isEscapeSolidus(),
+ isTrimStrings(),
+ true /* simpleAttrs — JSON5 sugar: unquoted field names
where safe */,
+ walk,
+ false /* disableObject */);
+ return JsonlTokenWriter.forOutput(output, settings);
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lTokenReader.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lTokenReader.java
new file mode 100644
index 0000000000..768402ae45
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/Json5lTokenReader.java
@@ -0,0 +1,78 @@
+/*
+ * 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.marshall.json5l;
+
+import java.io.*;
+
+import org.apache.juneau.marshall.json5.*;
+import org.apache.juneau.marshall.parser.*;
+import org.apache.juneau.marshall.stream.*;
+
+/**
+ * Reference implementation of the public {@link TokenReader} surface for the
JSON5L format.
+ *
+ * <p>
+ * Subclasses {@link Json5TokenReader} to inherit the JSON5 dialect
relaxations (single-quoted and
+ * bare-identifier strings/field names, trailing commas, missing values,
comments) and adds JSONL's
+ * flat top-level sequencing: each line's value is emitted in turn at {@link
#getDepth() depth} 0
+ * and {@link #next()} returns {@link TokenType#END_OF_STREAM} only when the
input is exhausted.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='warn'>This class is not thread safe.
+ * <li>The cursor is a true O(1)-memory streaming cursor ({@link
#isStreaming()} == <jk>true</jk>).
+ * </ul>
+ */
+@SuppressWarnings({
+ "resource" // The cursor's underlying ParserPipe is owned by the caller
via try-with-resources on the cursor itself; Eclipse JDT flags the inner pipe
as unclosed but that's by design.
+})
+public class Json5lTokenReader extends Json5TokenReader {
+
+ /**
+ * Constructor with default settings.
+ *
+ * @param pipe The parser input pipe to read from. Must not be
<jk>null</jk>.
+ * @throws IOException If a problem occurred opening the underlying
reader.
+ */
+ public Json5lTokenReader(ParserPipe pipe) throws IOException {
+ super(pipe);
+ }
+
+ /**
+ * Constructor used by {@link Json5lParserSession#parseTokens(Object)}
to plumb the calling
+ * session through so that {@link #read(Class)} can delegate to the
JSON5L databind path.
+ *
+ * @param pipe The parser input pipe. Must not be <jk>null</jk>.
+ * @param settings The cursor-level settings. Must not be
<jk>null</jk>.
+ * @param session The {@link Json5lParserSession} for {@link
#read(Class)} delegation, or
+ * <jk>null</jk> to disable {@code read}.
+ * @throws IOException If a problem occurred opening the underlying
reader.
+ */
+ public Json5lTokenReader(ParserPipe pipe, Settings settings,
Json5lParserSession session) throws IOException {
+ super(pipe, settings, session);
+ }
+
+ @Override /* JsonTokenReader */
+ protected void afterValue() {
+ // At depth 0, a top-level value just completed and we want the
next next() / read() to
+ // consume the next line's value (instead of transitioning to
S05_end as plain JSON would).
+ // Inside a container the parent's logic is still correct.
+ if (depth == 0)
+ state = S00_expectValue;
+ else
+ super.afterValue();
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/package-info.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/package-info.java
new file mode 100644
index 0000000000..00eb936686
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json5l/package-info.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+/**
+ * JSON5L (JSON5 Lines) marshalling support.
+ *
+ * <p>
+ * JSON5L combines the relaxed JSON5 dialect (comments, unquoted/single-quoted
keys, trailing
+ * commas, relaxed numbers) with JSONL's newline-delimited framing (one
document per line).
+ *
+ * <p>
+ * This package provides {@link
org.apache.juneau.marshall.json5l.Json5lSerializer} and
+ * {@link org.apache.juneau.marshall.json5l.Json5lParser}. The serializer
emits strict
+ * RFC-8259 JSON per line by default (byte-identical to {@link
org.apache.juneau.marshall.jsonl.JsonlSerializer}),
+ * with an opt-in for JSON5 sugar. The parser accepts the full JSON5 dialect
per line — and,
+ * because JSON5 is a strict superset of JSON, plain JSONL input as well.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Serialize a list of beans to JSON5L</jc>
+ * String <jv>json5l</jv> = Json5l.<jsm>of</jsm>(<jv>myList</jv>);
+ *
+ * <jc>// Parse JSON5L back to a list</jc>
+ * List<MyBean> <jv>list</jv> =
Json5l.<jsm>to</jsm>(<jv>json5l</jv>, List.<jk>class</jk>,
MyBean.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://jsonlines.org/">JSON
Lines Specification</a>
+ * <li class='link'><a class="doclink" href="https://json5.org/">JSON5
Specification</a>
+ * </ul>
+ */
+package org.apache.juneau.marshall.json5l;
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/marshaller/Json5l.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/marshaller/Json5l.java
new file mode 100644
index 0000000000..f0a278cf5a
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/marshaller/Json5l.java
@@ -0,0 +1,229 @@
+/*
+ * 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.marshall.marshaller;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.nio.charset.*;
+
+import org.apache.juneau.marshall.*;
+import org.apache.juneau.marshall.json5l.*;
+import org.apache.juneau.marshall.parser.*;
+import org.apache.juneau.marshall.serializer.*;
+
+/**
+ * Pairs {@link Json5lSerializer} and {@link Json5lParser} into a single class
with convenience
+ * read/write methods.
+ *
+ * <h5 class='figure'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Serialize to JSON5L using instance</jc>
+ * Json5l <jv>json5l</jv> = <jk>new</jk> Json5l();
+ * String <jv>out</jv> = <jv>json5l</jv>.write(<jv>myList</jv>);
+ * List<MyBean> <jv>in</jv> = <jv>json5l</jv>.read(<jv>out</jv>,
List.<jk>class</jk>, MyBean.<jk>class</jk>);
+ *
+ * <jc>// Serialize to JSON5L using DEFAULT instance</jc>
+ * String <jv>out</jv> = Json5l.<jsm>of</jsm>(<jv>myList</jv>);
+ * List<MyBean> <jv>in</jv> = Json5l.<jsm>to</jsm>(<jv>out</jv>,
List.<jk>class</jk>, MyBean.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='figure'>Example output (List of beans, strict-per-line
default):</h5>
+ * <p class='bjson'>
+ * {"name":"Alice","age":30}
+ * {"name":"Bob","age":25}
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/Marshallers">Marshallers</a>
+ * </ul>
+ */
+public class Json5l extends CharMarshaller {
+
+ /**
+ * Default reusable instance.
+ */
+ public static final Json5l DEFAULT = new Json5l();
+
+ /**
+ * Serializes a Java object to a JSON5L 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);
+ }
+
+ /**
+ * Serializes a Java object to a JSON5L output.
+ *
+ * <p>
+ * A shortcut for calling <c><jsf>DEFAULT</jsf>.write(<jv>object</jv>,
<jv>output</jv>)</c>.
+ *
+ * @param object The object to serialize.
+ * @param output
+ * The output object.
+ * <br>Can be any of the following types:
+ * <ul>
+ * <li>{@link Writer}
+ * <li>{@link OutputStream} - Output will be written as
UTF-8 encoded stream.
+ * <li>{@link File} - Output will be written as
system-default encoded stream.
+ * <li>{@link StringBuilder} - Output will be written to
the specified string builder.
+ * </ul>
+ * @return The output object.
+ * @throws SerializeException If a problem occurred trying to convert
the output.
+ * @throws IOException Thrown by underlying stream.
+ */
+ public static Object of(Object object, Object output) throws
SerializeException, IOException {
+ DEFAULT.write(object, output);
+ return output;
+ }
+
+ /**
+ * Parses a JSON5L input object 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.
+ * <br>Can be any of the following types:
+ * <ul>
+ * <li><jk>null</jk>
+ * <li>{@link Reader}
+ * <li>{@link CharSequence}
+ * <li>{@link InputStream} containing UTF-8 encoded text
(or charset defined by
+ * {@link
org.apache.juneau.marshall.parser.ReaderParser.Builder#streamCharset(Charset)}
property value).
+ * <li><code><jk>byte</jk>[]</code> containing UTF-8
encoded text (or charset defined by
+ * {@link
org.apache.juneau.marshall.parser.ReaderParser.Builder#streamCharset(Charset)}
property value).
+ * <li>{@link File} containing system encoded text (or
charset defined by
+ * {@link
org.apache.juneau.marshall.parser.ReaderParser.Builder#fileCharset(Charset)}
property value).
+ * </ul>
+ * @param type The object type to create.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ * @throws IOException Thrown by underlying stream.
+ */
+ public static <T> T to(Object input, Class<T> type) throws
ParseException, IOException {
+ return DEFAULT.read(input, type);
+ }
+
+ /**
+ * Parses a JSON5L input object to the specified Java type.
+ *
+ * <p>
+ * A shortcut for calling <c><jsf>DEFAULT</jsf>.read(<jv>input</jv>,
<jv>type</jv>, <jv>args</jv>)</c>.
+ *
+ * @param <T> The class type of the object to create.
+ * @param input
+ * The input.
+ * <br>Can be any of the following types:
+ * <ul>
+ * <li><jk>null</jk>
+ * <li>{@link Reader}
+ * <li>{@link CharSequence}
+ * <li>{@link InputStream} containing UTF-8 encoded text
(or charset defined by
+ * {@link
org.apache.juneau.marshall.parser.ReaderParser.Builder#streamCharset(java.nio.charset.Charset)}
property value).
+ * <li><code><jk>byte</jk>[]</code> containing UTF-8
encoded text (or charset defined by
+ * {@link
org.apache.juneau.marshall.parser.ReaderParser.Builder#streamCharset(java.nio.charset.Charset)}
property value).
+ * <li>{@link File} containing system encoded text (or
charset defined by
+ * {@link
org.apache.juneau.marshall.parser.ReaderParser.Builder#fileCharset(java.nio.charset.Charset)}
property value).
+ * </ul>
+ * @param type
+ * The object type to create.
+ * <br>Can be any of the following: {@link ClassMeta}, {@link
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+ * @param args
+ * The type arguments of the class if it's a collection or map.
+ * <br>Can be any of the following: {@link ClassMeta}, {@link
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+ * <br>Ignored if the main type is not a map or collection.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ * @throws IOException Thrown by underlying stream.
+ * @see MarshallingSession#getClassMeta(Type,Type...) for argument
syntax for maps and collections.
+ */
+ public static <T> T to(Object input, Type type, Type...args) throws
ParseException, IOException {
+ return DEFAULT.read(input, type, args);
+ }
+
+ /**
+ * Parses a JSON5L input string to the specified 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.
+ * @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);
+ }
+
+ /**
+ * Parses a JSON5L input string to the specified Java type.
+ *
+ * <p>
+ * A shortcut for calling <c><jsf>DEFAULT</jsf>.read(<jv>input</jv>,
<jv>type</jv>, <jv>args</jv>)</c>.
+ *
+ * @param <T> The class type of the object to create.
+ * @param input The input.
+ * @param type
+ * The object type to create.
+ * <br>Can be any of the following: {@link ClassMeta}, {@link
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+ * @param args
+ * The type arguments of the class if it's a collection or map.
+ * <br>Can be any of the following: {@link ClassMeta}, {@link
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+ * <br>Ignored if the main type is not a map or collection.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ * @see MarshallingSession#getClassMeta(Type,Type...) for argument
syntax for maps and collections.
+ */
+ public static <T> T to(String input, Type type, Type...args) throws
ParseException {
+ return DEFAULT.read(input, type, args);
+ }
+
+ /**
+ * Constructor.
+ *
+ * <p>
+ * Uses {@link Json5lSerializer#DEFAULT} and {@link
Json5lParser#DEFAULT}.
+ */
+ public Json5l() {
+ this(Json5lSerializer.DEFAULT, Json5lParser.DEFAULT);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param s
+ * The serializer to use for serializing output.
+ * <br>Must not be <jk>null</jk>.
+ * @param p
+ * The parser to use for parsing input.
+ * <br>Must not be <jk>null</jk>.
+ */
+ public Json5l(Json5lSerializer s, Json5lParser p) {
+ super(s, p);
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
index 4956344d33..ae837b791a 100644
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
@@ -194,6 +194,34 @@ public abstract class ComboRoundTripTest_Base extends
TestBase {
t.testParseVerify("jsonl");
}
+
//-----------------------------------------------------------------------------------------------------------------
+ // JSON5L
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a46_serializeJson5l(ComboRoundTrip_Tester<?> t) throws
Exception {
+ t.testSerialize("json5l");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a47_parseJson5l(ComboRoundTrip_Tester<?> t) throws
Exception {
+ t.testParse("json5l");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a48_parseJson5lJsonEquivalency(ComboRoundTrip_Tester<?> t)
throws Exception {
+ t.testParseJsonEquivalency("json5l");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a49_verifyJson5l(ComboRoundTrip_Tester<?> t) throws
Exception {
+ t.testParseVerify("json5l");
+ }
+
//-----------------------------------------------------------------------------------------------------------------
// XML
//-----------------------------------------------------------------------------------------------------------------
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
index 25055235cf..2424219306 100644
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
@@ -32,6 +32,7 @@ import org.apache.juneau.marshall.html.*;
import org.apache.juneau.marshall.ini.*;
import org.apache.juneau.marshall.json.*;
import org.apache.juneau.marshall.json5.*;
+import org.apache.juneau.marshall.json5l.*;
import org.apache.juneau.marshall.jsonl.*;
import org.apache.juneau.marshall.markdown.*;
import org.apache.juneau.marshall.msgpack.*;
@@ -113,6 +114,7 @@ public class ComboRoundTrip_Tester<T> {
public Builder<T> json5T(String value) { expected.put("json5T",
value); return this; }
public Builder<T> json5R(String value) { expected.put("json5R",
value); return this; }
public Builder<T> jsonl(String value) { expected.put("jsonl",
value); return this; }
+ public Builder<T> json5l(String value) { expected.put("json5l",
value); return this; }
public Builder<T> xml(String value) { expected.put("xml",
value); return this; }
public Builder<T> xmlT(String value) { expected.put("xmlT",
value); return this; }
public Builder<T> xmlR(String value) { expected.put("xmlR",
value); return this; }
@@ -171,6 +173,7 @@ public class ComboRoundTrip_Tester<T> {
serializers.put("json5T", create(b,
Json5Serializer.create().typePropertyName("t").addBeanTypes().addRootType()));
serializers.put("json5R", create(b,
Json5Serializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
serializers.put("jsonl", create(b,
JsonlSerializer.create().keepNullProperties().addBeanTypes().addRootType()));
+ serializers.put("json5l", create(b,
Json5lSerializer.create().keepNullProperties().addBeanTypes().addRootType()));
serializers.put("xml", create(b,
XmlSerializer.DEFAULT_SQ.copy().addBeanTypes().addRootType()));
serializers.put("xmlT", create(b,
XmlSerializer.create().sq().typePropertyName("t").addBeanTypes().addRootType()));
serializers.put("xmlR", create(b,
XmlSerializer.DEFAULT_SQ_READABLE.copy().addBeanTypes().addRootType()));
@@ -201,6 +204,7 @@ public class ComboRoundTrip_Tester<T> {
parsers.put("json5T", create(b,
Json5Parser.create().typePropertyName("t")));
parsers.put("json5R", create(b, Json5Parser.DEFAULT.copy()));
parsers.put("jsonl", create(b, JsonlParser.create()));
+ parsers.put("json5l", create(b, Json5lParser.create()));
parsers.put("xml", create(b, XmlParser.DEFAULT.copy()));
parsers.put("xmlT", create(b,
XmlParser.create().typePropertyName("t")));
parsers.put("xmlR", create(b, XmlParser.DEFAULT.copy()));
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lCoverage_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lCoverage_Test.java
new file mode 100644
index 0000000000..555056ed48
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lCoverage_Test.java
@@ -0,0 +1,199 @@
+/*
+ * 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.marshall.json5l;
+
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.marshall.parser.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Supplemental coverage tests for the {@code json5l} package — exercises edge
branches not covered
+ * by the primary behavioral tests.
+ */
+@SuppressWarnings({
+ "unchecked", // Parser returns Object; casts in tests
+ "resource" // Token readers/pipes are short-lived test fixtures.
+})
+class Json5lCoverage_Test extends TestBase {
+
+ @BeanType(properties = "name,age")
+ public static class Person {
+ public String name;
+ public int age;
+
+ public Person() {}
+ public Person(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+ }
+
+ //
=================================================================================
+ // A. Parser session — doParse array/null branches
+ //
=================================================================================
+
+ @Test
+ void a01_parseToArrayType() throws Exception {
+ var in = "{name:'Alice',age:30}\n{name:'Bob',age:25}";
+ var arr = Json5lParser.DEFAULT.parse(in, Person[].class);
+ assertBean(arr, "0{name,age},1{name,age}",
"{Alice,30},{Bob,25}");
+ }
+
+ @Test
+ void a02_singleTargetNoParseableLineReturnsNull() throws Exception {
+ // Only comment/blank lines → the single-value loop falls
through to return null.
+ var p = Json5lParser.DEFAULT.parse("// just a comment\n\n",
Person.class);
+ assertNull(p);
+ }
+
+ @Test
+ void a03_nullInputReturnsNull() throws Exception {
+ assertNull(Json5lParser.DEFAULT.parse((String) null,
Person.class));
+ }
+
+ //
=================================================================================
+ // B. isParseable — block-comment branch coverage
+ //
=================================================================================
+
+ @Test
+ void b01_blockCommentWithTrailingContentIsParseable() throws Exception {
+ // `/* ... */ {a:1}` — closing */ is NOT at end of line, so the
line IS parsed.
+ var in = "/* lead */ {name:'Alice',age:30}";
+ var list = (List<Person>) Json5lParser.DEFAULT.parse(in,
List.class, Person.class);
+ assertBean(list, "0{name,age}", "{Alice,30}");
+ }
+
+ @Test
+ void b02_lineNotStartingWithCommentIsParseable() throws Exception {
+ var in = "{name:'Alice',age:30}";
+ var list = (List<Person>) Json5lParser.DEFAULT.parse(in,
List.class, Person.class);
+ assertBean(list, "0{name}", "{Alice}");
+ }
+
+ //
=================================================================================
+ // C. Token reader — single-arg constructor + read() delegation disabled
+ //
=================================================================================
+
+ @Test
+ void c01_singleArgConstructor() throws Exception {
+ try (var pipe = new ParserPipe("{a:1}\n");
+ var r = new Json5lTokenReader(pipe)) {
+ assertNotNull(r.next());
+ }
+ }
+
+ //
=================================================================================
+ // D. Serializer / builder — copy() paths
+ //
=================================================================================
+
+ @Test
+ void d01_serializerBuilderCopy() {
+ var b = Json5lSerializer.create().json5Sugar();
+ var copy = b.copy();
+ assertTrue(copy.build().isJson5Sugar());
+ }
+
+ @Test
+ void d02_serializerContextCopy() {
+ var s = Json5lSerializer.create().json5Sugar().build();
+ assertTrue(s.copy().build().isJson5Sugar());
+ }
+
+ @Test
+ void d03_parserBuilderCopy() throws Exception {
+ var p = Json5lParser.create().copy().build();
+ var person = p.parse("{name:'Alice',age:30}", Person.class);
+ assertBean(person, "name,age", "Alice,30");
+ }
+
+ @Test
+ void d04_parserContextCopy() throws Exception {
+ var p = Json5lParser.DEFAULT.copy().build();
+ var person = p.parse("{name:'Bob',age:25}", Person.class);
+ assertBean(person, "name,age", "Bob,25");
+ }
+
+ //
=================================================================================
+ // E. Serializer session — getJsonWriter when output is already a
JsonWriter (sugar on)
+ //
=================================================================================
+
+ @Test
+ void e01_sugarSerializeViaStringRoundTrip() throws Exception {
+ // Drives the getJsonWriter() sugar branch through
serializeToString (which wraps a fresh writer).
+ var s = Json5lSerializer.create().json5Sugar().build();
+ var out = s.serialize(JsonMap.of("name", "Alice"));
+ assertEquals("{name:'Alice'}", out.trim());
+ }
+
+ @Test
+ void e02_strictSerializeViaStringUsesDoubleQuotes() throws Exception {
+ // Drives the getJsonWriter() strict (non-sugar) branch.
+ var out = Json5lSerializer.DEFAULT.serialize(JsonMap.of("name",
"Alice"));
+ assertEquals("{\"name\":\"Alice\"}", out.trim());
+ }
+
+ //
=================================================================================
+ // F. @Json5lConfig annotation appliers (no-op, but must be exercised)
+ //
=================================================================================
+
+ @Json5lConfig(rank = 1)
+ public static class F_Configured {}
+
+ @Test
+ void f01_serializerApply() {
+ var s =
Json5lSerializer.create().applyAnnotations(F_Configured.class).build();
+ assertNotNull(s);
+ }
+
+ @Test
+ void f02_parserApply() {
+ var p =
Json5lParser.create().applyAnnotations(F_Configured.class).build();
+ assertNotNull(p);
+ }
+
+ //
=================================================================================
+ // G. isParseable — remaining branch coverage
+ //
=================================================================================
+
+ @Test
+ void g01_blankLinesOnlyBetweenRecords() throws Exception {
+ // Blank line in the array loop hits the isEmpty()==true branch.
+ var list = (List<Person>)
Json5lParser.DEFAULT.parse("{name:'A'}\n \n{name:'B'}", List.class,
Person.class);
+ assertBean(list, "0{name},1{name}", "{A},{B}");
+ }
+
+ @Test
+ void g02_lineCommentInArrayLoopSkipped() throws Exception {
+ // `//`-prefixed line in the array loop hits the
startsWith("//")==true branch.
+ var list = (List<Person>)
Json5lParser.DEFAULT.parse("{name:'A'}\n// skip\n{name:'B'}", List.class,
Person.class);
+ assertBean(list, "0{name},1{name}", "{A},{B}");
+ }
+
+ @Test
+ void g03_blockCommentWithTextBeforeCloseIsParseable() throws Exception {
+ // `/* */ x` style: indexOf("*/") != end so the line is parsed
(the &&-chain's last branch).
+ var list = (List<Person>) Json5lParser.DEFAULT.parse("/* a
*/{name:'A'}", List.class, Person.class);
+ assertBean(list, "0{name}", "{A}");
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lMediaType_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lMediaType_Test.java
new file mode 100644
index 0000000000..3cc8a680b5
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lMediaType_Test.java
@@ -0,0 +1,73 @@
+/*
+ * 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.marshall.json5l;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.marshall.marshaller.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for JSON5L media type configuration.
+ */
+@SuppressWarnings({
+ "unchecked" // Parser returns Object; cast to List<JsonMap> in tests
+})
+class Json5lMediaType_Test {
+
+ @Test
+ void a01_producesCorrectMediaType() {
+ var ct = Json5lSerializer.DEFAULT.getResponseContentType();
+ assertEquals("application", ct.getType());
+ assertEquals("json5l", ct.getSubType());
+ }
+
+ @Test
+ void a02_acceptsAllMediaTypes() {
+ var types = new ArrayList<String>();
+ Json5lSerializer.DEFAULT.forEachAcceptMediaType(mt ->
types.add(mt.getType() + "/" + mt.getSubType()));
+ assertTrue(types.stream().anyMatch(t -> t.contains("json5l")),
"Expected application/json5l: " + types);
+ assertTrue(types.stream().anyMatch(t ->
"text/json5l".equals(t)), "Expected text/json5l: " + types);
+ // Cross-acceptance of the JSONL family (reduced q-value).
+ assertTrue(types.stream().anyMatch(t -> t.contains("jsonl")),
"Expected jsonl cross-accept: " + types);
+ assertTrue(types.stream().anyMatch(t -> t.contains("ndjson")),
"Expected ndjson cross-accept: " + types);
+ }
+
+ @Test
+ void a03_consumesAllMediaTypes() {
+ var types = Json5lParser.DEFAULT.getMediaTypes().stream()
+ .map(mt -> mt.getType() + "/" + mt.getSubType())
+ .toList();
+ assertTrue(types.stream().anyMatch(t -> t.contains("json5l")),
"Expected json5l types: " + types);
+ assertTrue(types.stream().anyMatch(t -> t.contains("jsonl")),
"Expected jsonl cross-accept: " + types);
+ assertTrue(types.stream().anyMatch(t -> t.contains("ndjson")),
"Expected ndjson cross-accept: " + types);
+ }
+
+ @Test
+ void a04_contentNegotiation() throws Exception {
+ var a = list(JsonMap.of("k", "v"));
+ var json5l = Json5l.of(a);
+ assertNotNull(json5l);
+ var b = (List<JsonMap>) Json5l.to(json5l, List.class,
JsonMap.class);
+ assertBean(b, "0{k}", "{v}");
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lParser_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lParser_Test.java
new file mode 100644
index 0000000000..8f5e62d783
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lParser_Test.java
@@ -0,0 +1,155 @@
+/*
+ * 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.marshall.json5l;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.marshall.marshaller.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link Json5lParser}.
+ */
+@SuppressWarnings({
+ "unchecked" // Parser returns Object; cast to
List<Person>/List<JsonMap>/List<String> in tests
+})
+class Json5lParser_Test extends TestBase {
+
+ @BeanType(properties = "name,age")
+ public static class Person {
+ public String name;
+ public int age;
+
+ public Person() {}
+ public Person(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+ }
+
+ //
=================================================================================
+ // A. Strict JSONL input (JSON5 is a superset of JSON)
+ //
=================================================================================
+
+ @Test
+ void a01_parseStrictJsonlToListOfBeans() throws Exception {
+ var in =
"{\"name\":\"Alice\",\"age\":30}\n{\"name\":\"Bob\",\"age\":25}";
+ var list = (List<Person>) Json5l.to(in, List.class,
Person.class);
+ assertBean(list, "0{name,age},1{name,age}",
"{Alice,30},{Bob,25}");
+ }
+
+ @Test
+ void a02_parseStrictSingleLine() throws Exception {
+ var p = Json5l.to("{\"name\":\"Alice\",\"age\":30}",
Person.class);
+ assertBean(p, "name,age", "Alice,30");
+ }
+
+ //
=================================================================================
+ // B. JSON5 dialect, one document per line
+ //
=================================================================================
+
+ @Test
+ void b01_unquotedKeysAndSingleQuotes() throws Exception {
+ var in = "{name:'Alice',age:30}\n{name:'Bob',age:25}";
+ var list = (List<Person>) Json5l.to(in, List.class,
Person.class);
+ assertBean(list, "0{name,age},1{name,age}",
"{Alice,30},{Bob,25}");
+ }
+
+ @Test
+ void b02_trailingCommas() throws Exception {
+ var in = "{name:'Alice',age:30,}\n{name:'Bob',age:25,}";
+ var list = (List<Person>) Json5l.to(in, List.class,
Person.class);
+ assertBean(list, "0{name,age},1{name,age}",
"{Alice,30},{Bob,25}");
+ }
+
+ @Test
+ void b03_mixedStrictAndSugarLines() throws Exception {
+ var in = "{name:'Alice',age:30}\n{\"name\":\"Bob\",\"age\":25}";
+ var list = (List<Person>) Json5l.to(in, List.class,
Person.class);
+ assertBean(list, "0{name,age},1{name,age}",
"{Alice,30},{Bob,25}");
+ }
+
+ //
=================================================================================
+ // C. Comment handling
+ //
=================================================================================
+
+ @Test
+ void c01_commentOnlyLineSkipped() throws Exception {
+ var in = "// header
comment\n{name:'Alice',age:30}\n{name:'Bob',age:25}";
+ var list = (List<Person>) Json5l.to(in, List.class,
Person.class);
+ assertBean(list, "0{name},1{name}", "{Alice},{Bob}");
+ }
+
+ @Test
+ void c02_blockCommentOnlyLineSkipped() throws Exception {
+ var in = "/* block comment */\n{name:'Alice',age:30}";
+ var list = (List<Person>) Json5l.to(in, List.class,
Person.class);
+ assertBean(list, "0{name}", "{Alice}");
+ }
+
+ @Test
+ void c03_inlineTrailingLineComment() throws Exception {
+ var in = "{name:'Alice',age:30} //
trailing\n{name:'Bob',age:25}";
+ var list = (List<Person>) Json5l.to(in, List.class,
Person.class);
+ assertBean(list, "0{name},1{name}", "{Alice},{Bob}");
+ }
+
+ @Test
+ void c04_blankAndCommentLinesInterspersed() throws Exception {
+ var in = "\n// one\n{name:'Alice'}\n\n/* two
*/\n{name:'Bob'}\n";
+ var list = (List<Person>) Json5l.to(in, List.class,
Person.class);
+ assertBean(list, "0{name},1{name}", "{Alice},{Bob}");
+ }
+
+ @Test
+ void c05_commentOnlySingleObjectTarget() throws Exception {
+ var in = "// nothing but a comment
first\n{name:'Alice',age:30}";
+ var p = Json5l.to(in, Person.class);
+ assertBean(p, "name,age", "Alice,30");
+ }
+
+ //
=================================================================================
+ // D. Empty / blank inputs
+ //
=================================================================================
+
+ @Test
+ void d01_parseEmptyInput() throws Exception {
+ var list = (List<?>) Json5l.to("", List.class, Person.class);
+ assertNotNull(list);
+ assertTrue(list.isEmpty());
+ }
+
+ @Test
+ void d02_parseCommentOnlyInputToList() throws Exception {
+ var list = (List<?>) Json5l.to("// only a comment\n\n",
List.class, Person.class);
+ assertNotNull(list);
+ assertTrue(list.isEmpty());
+ }
+
+ @Test
+ void d03_parseToListOfStrings() throws Exception {
+ var in = "'foo'\n'bar'\n'baz'";
+ var list = (List<String>) Json5l.to(in, List.class,
String.class);
+ assertEquals(list("foo", "bar", "baz"), list);
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lRoundTrip_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lRoundTrip_Test.java
new file mode 100644
index 0000000000..586eec3c60
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lRoundTrip_Test.java
@@ -0,0 +1,78 @@
+/*
+ * 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.marshall.json5l;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.marshall.collections.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Round-trip tests for {@link Json5lSerializer} / {@link Json5lParser}.
+ */
+@SuppressWarnings({
+ "unchecked" // Parser returns Object; cast to
List<Person>/List<JsonMap> in tests
+})
+class Json5lRoundTrip_Test extends TestBase {
+
+ @BeanType(properties = "name,age")
+ public static class Person {
+ public String name;
+ public int age;
+
+ public Person() {}
+ public Person(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+ }
+
+ @Test
+ void a01_strictRoundTrip() throws Exception {
+ var in = list(new Person("Alice", 30), new Person("Bob", 25));
+ var out = Json5lSerializer.DEFAULT.serialize(in);
+ var back = (List<Person>) Json5lParser.DEFAULT.parse(out,
List.class, Person.class);
+ assertBean(back, "0{name,age},1{name,age}",
"{Alice,30},{Bob,25}");
+ }
+
+ @Test
+ void a02_sugarRoundTrip() throws Exception {
+ var s = Json5lSerializer.create().json5Sugar().build();
+ var in = list(new Person("Alice", 30), new Person("Bob", 25));
+ var out = s.serialize(in);
+ // Sugar output is single-quoted / unquoted-key; the parser
reads it back fine.
+ assertTrue(out.contains("name:'Alice'"));
+ var back = (List<Person>) Json5lParser.DEFAULT.parse(out,
List.class, Person.class);
+ assertBean(back, "0{name,age},1{name,age}",
"{Alice,30},{Bob,25}");
+ }
+
+ @Test
+ void a03_mapRoundTrip() throws Exception {
+ var in = list(JsonMap.of("x", 1), JsonMap.of("y", 2));
+ var out = Json5lSerializer.DEFAULT.serialize(in);
+ var back = (List<JsonMap>) Json5lParser.DEFAULT.parse(out,
List.class, JsonMap.class);
+ assertEquals(2, back.size());
+ assertEquals(1, back.get(0).getInt("x"));
+ assertEquals(2, back.get(1).getInt("y"));
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lTokenStream_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lTokenStream_Test.java
new file mode 100644
index 0000000000..db46bfc3d7
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5lTokenStream_Test.java
@@ -0,0 +1,136 @@
+/*
+ * 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.marshall.json5l;
+
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.apache.juneau.marshall.stream.TokenStreamAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.stream.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Unit tests for the public JSON5L token-streaming surface.
+ */
+@SuppressWarnings({
+ "resource" // Token readers are closed via try-with-resources; JDT's
flow analysis over chained factory calls yields false-positive leak reports.
+})
+class Json5lTokenStream_Test extends TestBase {
+
+ //
=================================================================================
+ // A. Reader — JSON5L flat-sequence semantics with JSON5 dialect
+ //
=================================================================================
+
+ @Nested class A_reader extends TestBase {
+
+ @Test void a01_singleLine() throws Exception {
+ try (var r =
Json5lParser.DEFAULT.parseTokens("{a:1}\n")) {
+ assertSequence(r,
+ TokenType.START_OBJECT,
+ TokenType.FIELD_NAME,
+ TokenType.VALUE_NUMBER,
+ TokenType.END_OBJECT,
+ TokenType.END_OF_STREAM);
+ }
+ }
+
+ @Test void a02_multipleLinesFlat() throws Exception {
+ try (var r =
Json5lParser.DEFAULT.parseTokens("{a:1}\n{b:2}\n")) {
+ assertSequence(r,
+ TokenType.START_OBJECT,
TokenType.FIELD_NAME, TokenType.VALUE_NUMBER, TokenType.END_OBJECT,
+ TokenType.START_OBJECT,
TokenType.FIELD_NAME, TokenType.VALUE_NUMBER, TokenType.END_OBJECT,
+ TokenType.END_OF_STREAM);
+ }
+ }
+
+ @Test void a03_capability() throws Exception {
+ assertInstanceOf(TokenReadable.class,
Json5lParser.DEFAULT);
+ try (var r = Json5lParser.DEFAULT.parseTokens("1\n")) {
+ assertReaderStreaming(r);
+ }
+ }
+ }
+
+ //
=================================================================================
+ // B. Writer — strict default and json5Sugar output, newline per
top-level value
+ //
=================================================================================
+
+ @Nested class B_writer extends TestBase {
+
+ @Test void b01_strictDefault() throws Exception {
+ var sb = new StringBuilder();
+ try (var w =
Json5lSerializer.DEFAULT.serializeTokens(sb)) {
+ w.startObject(); w.fieldName("a"); w.number(1);
w.endObject();
+ w.startObject(); w.fieldName("b"); w.number(2);
w.endObject();
+ }
+ assertEquals("{\"a\":1}\n{\"b\":2}\n", sb.toString());
+ }
+
+ @Test void b02_sugarUnquotedKeysSingleQuotes() throws Exception
{
+ var s = Json5lSerializer.create().json5Sugar().build();
+ var sb = new StringBuilder();
+ try (var w = s.serializeTokens(sb)) {
+ w.startObject(); w.fieldName("a");
w.string("x"); w.endObject();
+ w.startObject(); w.fieldName("b");
w.string("y"); w.endObject();
+ }
+ assertEquals("{a:'x'}\n{b:'y'}\n", sb.toString());
+ }
+
+ @Test void b03_capability() throws Exception {
+ assertInstanceOf(TokenWritable.class,
Json5lSerializer.DEFAULT);
+ var sb = new StringBuilder();
+ try (var w =
Json5lSerializer.DEFAULT.serializeTokens(sb)) {
+ assertWriterStreaming(w);
+ }
+ }
+ }
+
+ //
=================================================================================
+ // C. Array-record stream — JSON5L aliases its line record stream (no
surrounding [...])
+ //
=================================================================================
+
+ @Nested class C_arrayRecords extends TestBase {
+
+ @Test void c01_capability() {
+ assertInstanceOf(ArrayRecordReadable.class,
Json5lParser.DEFAULT);
+ assertInstanceOf(ArrayRecordWritable.class,
Json5lSerializer.DEFAULT);
+ assertTrue(((ArrayRecordReadable)
Json5lParser.DEFAULT).isArrayRecordStreaming());
+ assertTrue(((ArrayRecordWritable)
Json5lSerializer.DEFAULT).isArrayRecordStreaming());
+ }
+
+ @Test void c02_roundTrip() throws Exception {
+ var sb = new StringBuilder();
+ try (RecordWriter w =
Json5lSerializer.DEFAULT.serializeArrayRecords(sb)) {
+ assertTrue(w.isStreaming());
+ w.write(java.util.Map.of("x", 1));
+ w.write(java.util.Map.of("x", 2));
+ }
+ assertString("{\"x\":1}\n{\"x\":2}\n", sb);
+
+ var records = new java.util.ArrayList<java.util.Map<?,
?>>();
+ try (RecordReader r =
Json5lParser.DEFAULT.parseArrayRecords(sb.toString())) {
+ assertTrue(r.isStreaming());
+ while (r.canRead())
+
records.add(r.read(java.util.Map.class));
+ }
+ assertEquals(2, records.size());
+ assertBean(records.get(0), "x", "1");
+ assertBean(records.get(1), "x", "2");
+ }
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5l_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5l_Test.java
new file mode 100644
index 0000000000..188a390c84
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json5l/Json5l_Test.java
@@ -0,0 +1,182 @@
+/*
+ * 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.marshall.json5l;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.marshall.marshaller.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link Json5lSerializer}.
+ */
+class Json5l_Test extends TestBase {
+
+ @BeanType(properties = "name,age")
+ public static class Person {
+ public String name;
+ public int age;
+
+ public Person() {}
+ public Person(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+ }
+
+ //
=================================================================================
+ // A. Serialization — strict-per-line default (mirrors JSONL semantics)
+ //
=================================================================================
+
+ @Test
+ void a01_serializeCollectionOfBeans() throws Exception {
+ var list = list(
+ new Person("Alice", 30),
+ new Person("Bob", 25),
+ new Person("Carol", 35)
+ );
+ var json5l = Json5l.of(list);
+ var lines = json5l.split("\n");
+ assertEquals(3, lines.length);
+ assertTrue(lines[0].contains("\"name\":\"Alice\""));
+ assertTrue(lines[0].contains("\"age\":30"));
+ assertTrue(lines[1].contains("\"name\":\"Bob\""));
+ assertTrue(lines[2].contains("\"name\":\"Carol\""));
+ assertFalse(json5l.contains("["));
+ assertFalse(json5l.contains("]"));
+ }
+
+ @Test
+ void a02_serializeArray() throws Exception {
+ var arr = new Person[]{new Person("Alice", 30), new
Person("Bob", 25)};
+ var json5l = Json5l.of(arr);
+ var lines = json5l.split("\n");
+ assertEquals(2, lines.length);
+ assertTrue(lines[0].contains("\"Alice\""));
+ assertTrue(lines[1].contains("\"Bob\""));
+ }
+
+ @Test
+ void a03_serializeSingleBean() throws Exception {
+ var p = new Person("Alice", 30);
+ var json5l = Json5l.of(p);
+ assertEquals(1, json5l.split("\n").length);
+ assertTrue(json5l.contains("\"name\":\"Alice\""));
+ assertTrue(json5l.contains("\"age\":30"));
+ }
+
+ @Test
+ void a04_serializeCollectionOfStrings() throws Exception {
+ var list = list("foo", "bar", "baz");
+ var json5l = Json5l.of(list);
+ var lines = json5l.split("\n");
+ assertEquals(3, lines.length);
+ assertTrue(lines[0].contains("\"foo\""));
+ assertTrue(lines[1].contains("\"bar\""));
+ assertTrue(lines[2].contains("\"baz\""));
+ }
+
+ @Test
+ void a05_serializeEmptyCollection() throws Exception {
+ var json5l = Json5l.of(list());
+ assertEquals("", json5l.trim());
+ }
+
+ @Test
+ void a06_serializeNullValues() throws Exception {
+ var list = list("a", null, "c");
+ var json5l = Json5l.of(list);
+ var lines = json5l.split("\n");
+ assertEquals(3, lines.length);
+ assertEquals("null", lines[1]);
+ }
+
+ @Test
+ void a07_serializeNestedObjects() throws Exception {
+ var outer = JsonMap.of("name", "Alice", "inner",
JsonMap.of("x", 1, "y", 2));
+ var json5l = Json5l.of(outer);
+ assertTrue(json5l.contains("\"inner\":{\"x\":1,\"y\":2}"));
+ }
+
+ //
=================================================================================
+ // B. Default output is byte-identical to JSONL (sugar off)
+ //
=================================================================================
+
+ @Test
+ void b01_defaultIsByteIdenticalToJsonl() throws Exception {
+ var list = list(
+ new Person("Alice", 30),
+ new Person("Bob", 25)
+ );
+ assertEquals(Jsonl.of(list), Json5l.of(list));
+ }
+
+ @Test
+ void b02_defaultProducesStrictDoubleQuotes() throws Exception {
+ var json5l = Json5l.of(JsonMap.of("name", "Alice"));
+ assertTrue(json5l.contains("\"name\":\"Alice\""));
+ assertFalse(json5l.contains("'"));
+ }
+
+ //
=================================================================================
+ // C. json5Sugar opt-in
+ //
=================================================================================
+
+ @Test
+ void c01_sugarUsesSingleQuotesAndUnquotedKeys() throws Exception {
+ var s = Json5lSerializer.create().json5Sugar().build();
+ var out = s.serialize(JsonMap.of("name", "Alice", "age", 30));
+ assertTrue(out.contains("name:'Alice'"), "Expected unquoted key
+ single quotes: " + out);
+ assertTrue(out.contains("age:30"), "Expected unquoted key: " +
out);
+ assertFalse(out.contains("\""), "Expected no double quotes: " +
out);
+ }
+
+ @Test
+ void c02_sugarStillOneLinePerRecord() throws Exception {
+ var s = Json5lSerializer.create().json5Sugar().build();
+ var out = s.serialize(list(new Person("Alice", 30), new
Person("Bob", 25)));
+ var lines = out.split("\n");
+ assertEquals(2, lines.length);
+ assertTrue(lines[0].contains("name:'Alice'"));
+ assertTrue(lines[1].contains("name:'Bob'"));
+ }
+
+ @Test
+ void c03_sugarFlagIsReflectedOnContext() {
+ assertFalse(Json5lSerializer.DEFAULT.isJson5Sugar());
+
assertTrue(Json5lSerializer.create().json5Sugar().build().isJson5Sugar());
+ }
+
+ @Test
+ void c04_sugarAndStrictAreDistinctCachedInstances() {
+ var strict = Json5lSerializer.create().build();
+ var sugar = Json5lSerializer.create().json5Sugar().build();
+ assertNotSame(strict, sugar);
+ assertFalse(strict.isJson5Sugar());
+ assertTrue(sugar.isJson5Sugar());
+ }
+
+ @Test
+ void c05_copyPreservesSugar() {
+ var sugar = Json5lSerializer.create().json5Sugar().build();
+ assertTrue(sugar.copy().build().isJson5Sugar());
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshaller/Json5l_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshaller/Json5l_Test.java
new file mode 100644
index 0000000000..7f70c677ad
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshaller/Json5l_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.marshaller;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.marshall.marshaller.*;
+import org.junit.jupiter.api.*;
+
+@SuppressWarnings({
+ "unchecked", // Parser returns Object; cast to Map/List<JsonMap> in
tests
+ "resource" // Stream/reader instances are intentional short-lived
test fixtures; auto-close not required for these assertions.
+})
+class Json5l_Test extends TestBase {
+
+ @Test void a01_of() throws Exception {
+ var a = "foo";
+ var b = JsonMap.of("foo", "bar");
+ var c = list(JsonMap.of("a", 1), JsonMap.of("b", 2));
+
+ assertEquals("\"foo\"", Json5l.of(a).trim());
+ assertEquals("\"foo\"", Json5l.of(a,
stringWriter()).toString().trim());
+ assertTrue(Json5l.of(b).contains("\"foo\":\"bar\""));
+ var sw = new StringWriter();
+ Json5l.of(b, sw);
+ assertTrue(sw.toString().contains("\"foo\":\"bar\""));
+ var json5l = Json5l.of(c);
+ assertTrue(json5l.contains("\"a\":1"));
+ assertTrue(json5l.contains("\"b\":2"));
+ assertEquals(2, json5l.split("\n").length);
+ }
+
+ @Test void a02_to() throws Exception {
+ var a = "'foo'";
+ var b = "{foo:'bar'}";
+ var c = "{a:1}\n{b:2}";
+
+ assertEquals("foo", Json5l.to(a, String.class));
+ assertEquals("foo", Json5l.to(stringReader(a), String.class));
+ var m = (Map<String,String>) Json5l.to(b, Map.class,
String.class, String.class);
+ assertEquals("bar", m.get("foo"));
+ m = (Map<String,String>) Json5l.to(stringReader(b), Map.class,
String.class, String.class);
+ assertEquals("bar", m.get("foo"));
+ var list = (List<JsonMap>) Json5l.to(c, List.class,
JsonMap.class);
+ assertEquals(2, list.size());
+ assertEquals(1, list.get(0).getInt("a"));
+ assertEquals(2, list.get(1).getInt("b"));
+ }
+
+ @Test void a03_roundTrip() throws Exception {
+ var a = list(JsonMap.of("x", 1), JsonMap.of("y", 2));
+ var json5l = Json5l.of(a);
+ var b = (List<JsonMap>) Json5l.to(json5l, List.class,
JsonMap.class);
+ assertEquals(2, b.size());
+ assertEquals(1, b.get(0).getInt("x"));
+ assertEquals(2, b.get(1).getInt("y"));
+ }
+
+ @Test void a04_defaultInstance() throws Exception {
+ var a = list(JsonMap.of("k", "v"));
+ var json5l = Json5l.DEFAULT.write(a);
+ assertTrue(json5l.contains("\"k\":\"v\""));
+ var b = (List<JsonMap>) Json5l.DEFAULT.read(json5l, List.class,
JsonMap.class);
+ assertEquals(1, b.size());
+ assertEquals("v", b.get(0).getString("k"));
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Helper methods
+
//-----------------------------------------------------------------------------------------------------------------
+
+ private static Writer stringWriter() {
+ return new StringWriter();
+ }
+
+ private static Reader stringReader(String s) {
+ return new StringReader(s);
+ }
+}
diff --git
a/juneau-integration-tests/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
b/juneau-integration-tests/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
index 4956344d33..ae837b791a 100644
---
a/juneau-integration-tests/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
+++
b/juneau-integration-tests/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
@@ -194,6 +194,34 @@ public abstract class ComboRoundTripTest_Base extends
TestBase {
t.testParseVerify("jsonl");
}
+
//-----------------------------------------------------------------------------------------------------------------
+ // JSON5L
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a46_serializeJson5l(ComboRoundTrip_Tester<?> t) throws
Exception {
+ t.testSerialize("json5l");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a47_parseJson5l(ComboRoundTrip_Tester<?> t) throws
Exception {
+ t.testParse("json5l");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a48_parseJson5lJsonEquivalency(ComboRoundTrip_Tester<?> t)
throws Exception {
+ t.testParseJsonEquivalency("json5l");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a49_verifyJson5l(ComboRoundTrip_Tester<?> t) throws
Exception {
+ t.testParseVerify("json5l");
+ }
+
//-----------------------------------------------------------------------------------------------------------------
// XML
//-----------------------------------------------------------------------------------------------------------------
diff --git
a/juneau-integration-tests/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
b/juneau-integration-tests/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
index 0e3b0ea617..117b81f588 100644
---
a/juneau-integration-tests/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
+++
b/juneau-integration-tests/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
@@ -33,6 +33,7 @@ import org.apache.juneau.marshall.ini.*;
import org.apache.juneau.marshall.jena.*;
import org.apache.juneau.marshall.json.*;
import org.apache.juneau.marshall.json5.*;
+import org.apache.juneau.marshall.json5l.*;
import org.apache.juneau.marshall.jsonl.*;
import org.apache.juneau.marshall.markdown.*;
import org.apache.juneau.marshall.msgpack.*;
@@ -114,6 +115,7 @@ public class ComboRoundTrip_Tester<T> {
public Builder<T> json5T(String value) { expected.put("json5T",
value); return this; }
public Builder<T> json5R(String value) { expected.put("json5R",
value); return this; }
public Builder<T> jsonl(String value) { expected.put("jsonl",
value); return this; }
+ public Builder<T> json5l(String value) { expected.put("json5l",
value); return this; }
public Builder<T> xml(String value) { expected.put("xml",
value); return this; }
public Builder<T> xmlT(String value) { expected.put("xmlT",
value); return this; }
public Builder<T> xmlR(String value) { expected.put("xmlR",
value); return this; }
@@ -188,6 +190,7 @@ public class ComboRoundTrip_Tester<T> {
serializers.put("json5T", create(b,
Json5Serializer.create().typePropertyName("t").addBeanTypes().addRootType()));
serializers.put("json5R", create(b,
Json5Serializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
serializers.put("jsonl", create(b,
JsonlSerializer.create().keepNullProperties().addBeanTypes().addRootType()));
+ serializers.put("json5l", create(b,
Json5lSerializer.create().keepNullProperties().addBeanTypes().addRootType()));
serializers.put("xml", create(b,
XmlSerializer.DEFAULT_SQ.copy().addBeanTypes().addRootType()));
serializers.put("xmlT", create(b,
XmlSerializer.create().sq().typePropertyName("t").addBeanTypes().addRootType()));
serializers.put("xmlR", create(b,
XmlSerializer.DEFAULT_SQ_READABLE.copy().addBeanTypes().addRootType()));
@@ -234,6 +237,7 @@ public class ComboRoundTrip_Tester<T> {
parsers.put("json5T", create(b,
Json5Parser.create().typePropertyName("t")));
parsers.put("json5R", create(b, Json5Parser.DEFAULT.copy()));
parsers.put("jsonl", create(b, JsonlParser.create()));
+ parsers.put("json5l", create(b, Json5lParser.create()));
parsers.put("xml", create(b, XmlParser.DEFAULT.copy()));
parsers.put("xmlT", create(b,
XmlParser.create().typePropertyName("t")));
parsers.put("xmlR", create(b, XmlParser.DEFAULT.copy()));
diff --git
a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
index 61b4ca417c..3618cb649c 100644
---
a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
+++
b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
@@ -95,6 +95,7 @@ import org.apache.juneau.marshall.ini.*;
import org.apache.juneau.marshall.jcs.*;
import org.apache.juneau.marshall.json.*;
import org.apache.juneau.marshall.json5.*;
+import org.apache.juneau.marshall.json5l.*;
import org.apache.juneau.marshall.jsonl.*;
import org.apache.juneau.marshall.markdown.*;
import org.apache.juneau.marshall.marshaller.*;
@@ -5359,6 +5360,7 @@ public class RestClient extends MarshallingContextable
implements HttpClient, Cl
JsonSerializer.class,
JcsSerializer.class,
Json5Serializer.class,
+ Json5lSerializer.class,
JsonlSerializer.class,
HtmlSerializer.class,
XmlSerializer.class,
@@ -5380,6 +5382,7 @@ public class RestClient extends MarshallingContextable
implements HttpClient, Cl
.parsers(
JsonParser.class,
Json5Parser.class,
+ Json5lParser.class,
JsonlParser.class,
XmlParser.class,
HtmlParser.class,
diff --git
a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/ContentType.java
b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/ContentType.java
index 890e474dc4..43d7cc8a64 100644
---
a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/ContentType.java
+++
b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/ContentType.java
@@ -54,6 +54,8 @@ public class ContentType extends HttpMediaTypeHeader {
public static final ContentType APPLICATION_JSON = new
ContentType("application/json");
/** Content-Type for {@code application/json5} (Juneau {@code
Json5Serializer}/{@code Json5Parser}). */
public static final ContentType APPLICATION_JSON5 = new
ContentType("application/json5");
+ /** Content-Type for {@code application/json5l} (Juneau {@code
Json5lSerializer}/{@code Json5lParser}). */
+ public static final ContentType APPLICATION_JSON5L = new
ContentType("application/json5l");
/** Content-Type for {@code application/jsonl} (Juneau {@code
JsonlSerializer}/{@code JsonlParser}). */
public static final ContentType APPLICATION_JSONL = new
ContentType("application/jsonl");
/** Content-Type for {@code application/json-patch+json} (RFC 6902). */
diff --git
a/juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/header/ContentType_Test.java
b/juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/header/ContentType_Test.java
index 3ecfb70f4c..915b58c0c5 100644
---
a/juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/header/ContentType_Test.java
+++
b/juneau-rest/juneau-rest-common/src/test/java/org/apache/juneau/http/header/ContentType_Test.java
@@ -72,6 +72,7 @@ class ContentType_Test extends TestBase {
Arguments.of("APPLICATION_JCS_JSON",
ContentType.APPLICATION_JCS_JSON, "application/jcs+json"),
Arguments.of("APPLICATION_JSON",
ContentType.APPLICATION_JSON, "application/json"),
Arguments.of("APPLICATION_JSON5",
ContentType.APPLICATION_JSON5, "application/json5"),
+ Arguments.of("APPLICATION_JSON5L",
ContentType.APPLICATION_JSON5L, "application/json5l"),
Arguments.of("APPLICATION_JSONL",
ContentType.APPLICATION_JSONL, "application/jsonl"),
Arguments.of("APPLICATION_JSON_PATCH",
ContentType.APPLICATION_JSON_PATCH, "application/json-patch+json"),
Arguments.of("APPLICATION_LD_JSON",
ContentType.APPLICATION_LD_JSON, "application/ld+json"),
diff --git
a/juneau-rest/juneau-rest-server-reactive/src/main/java/org/apache/juneau/rest/server/reactive/ReactiveResponseProcessor.java
b/juneau-rest/juneau-rest-server-reactive/src/main/java/org/apache/juneau/rest/server/reactive/ReactiveResponseProcessor.java
index 60b3b4e0f7..defe4d8daa 100644
---
a/juneau-rest/juneau-rest-server-reactive/src/main/java/org/apache/juneau/rest/server/reactive/ReactiveResponseProcessor.java
+++
b/juneau-rest/juneau-rest-server-reactive/src/main/java/org/apache/juneau/rest/server/reactive/ReactiveResponseProcessor.java
@@ -72,7 +72,7 @@ import jakarta.servlet.http.*;
* <li><b>SSE</b> ({@code text/event-stream}) — each element is
emitted as a Server-Sent-Events
* frame. {@link SseEvent} elements are written verbatim; any
other element type is JSON-encoded
* into the {@code data:} field.
- * <li><b>NDJSON</b> ({@code application/x-ndjson}, {@code
application/jsonl}) — each element is
+ * <li><b>NDJSON</b> ({@code application/x-ndjson}, {@code
application/jsonl}, {@code application/json5l}) — each element is
* JSON-encoded on its own line.
* <li><b>Buffer</b> (default, any other media type) — all elements
are collected into a
* {@link java.util.List List} and serialized through the normal
serializer chain (e.g. a JSON
@@ -312,7 +312,7 @@ public class ReactiveResponseProcessor implements
ResponseProcessor {
var probe = ((ct == null ? "" : ct) + "," + (accept == null ?
"" : accept)).toLowerCase(Locale.ROOT);
if (probe.contains("event-stream"))
return Shape.SSE;
- if (probe.contains("ndjson") || probe.contains("jsonl") ||
probe.contains("json-seq"))
+ if (probe.contains("ndjson") || probe.contains("jsonl") ||
probe.contains("json5l") || probe.contains("json-seq"))
return Shape.NDJSON;
return Shape.BUFFER;
}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/config/BasicUniversalConfig.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/config/BasicUniversalConfig.java
index 16a2de8d2a..49d5cbe6ce 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/config/BasicUniversalConfig.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/config/BasicUniversalConfig.java
@@ -28,6 +28,7 @@ import org.apache.juneau.marshall.ini.*;
import org.apache.juneau.marshall.jcs.*;
import org.apache.juneau.marshall.json.*;
import org.apache.juneau.marshall.json5.*;
+import org.apache.juneau.marshall.json5l.*;
import org.apache.juneau.marshall.jsonl.*;
import org.apache.juneau.marshall.markdown.*;
import org.apache.juneau.marshall.msgpack.*;
@@ -62,6 +63,7 @@ import org.apache.juneau.rest.server.servlet.*;
* <li class='jc'>{@link
HtmlSchemaDocSerializer}
* <li class='jc'>{@link JsonSerializer}
* <li class='jc'>{@link Json5Serializer}
+ * <li class='jc'>{@link Json5lSerializer}
* <li class='jc'>{@link
JsonSchemaSerializer}
* <li class='jc'>{@link XmlDocSerializer}
* <li class='jc'>{@link UonSerializer}
@@ -76,6 +78,7 @@ import org.apache.juneau.rest.server.servlet.*;
* <ul class='javatree'>
* <li class='jc'>{@link JsonParser}
* <li class='jc'>{@link Json5Parser}
+ * <li class='jc'>{@link Json5lParser}
* <li class='jc'>{@link XmlParser}
* <li class='jc'>{@link HtmlParser}
* <li class='jc'>{@link UonParser}
@@ -153,6 +156,7 @@ import org.apache.juneau.rest.server.servlet.*;
JsonSerializer.class,
JcsSerializer.class,
Json5Serializer.class,
+ Json5lSerializer.class,
JsonlSerializer.class,
JsonSchemaSerializer.class,
XmlDocSerializer.class,
@@ -177,6 +181,7 @@ import org.apache.juneau.rest.server.servlet.*;
parsers={
JsonParser.class,
Json5Parser.class,
+ Json5lParser.class,
JsonlParser.class,
XmlParser.class,
HtmlParser.class,