You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/03/04 14:30:16 UTC
(juneau) branch master updated: JSONL support
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 6ef28a190a JSONL support
6ef28a190a is described below
commit 6ef28a190a0362ea516c2e6af3a40d513ae47d45
Author: James Bognar <ja...@salesforce.com>
AuthorDate: Wed Mar 4 09:30:12 2026 -0500
JSONL support
---
RELEASE-NOTES.txt | 10 +
.../src/main/java/org/apache/juneau/Context.java | 2 +
.../java/org/apache/juneau/jsonl/JsonlParser.java | 119 +++
.../apache/juneau/jsonl/JsonlParserSession.java | 133 +++
.../org/apache/juneau/jsonl/JsonlSerializer.java | 130 +++
.../juneau/jsonl/JsonlSerializerSession.java | 118 ++
.../juneau/jsonl/annotation/JsonlConfig.java | 47 +
.../jsonl/annotation/JsonlConfigAnnotation.java | 71 ++
.../java/org/apache/juneau/jsonl/package-info.java | 43 +
.../java/org/apache/juneau/marshaller/Jsonl.java | 236 ++++
.../org/apache/juneau/rest/client/RestClient.java | 3 +
.../juneau/rest/config/BasicUniversalConfig.java | 3 +
.../org/apache/juneau/ComboRoundTripTest_Base.java | 28 +
.../org/apache/juneau/ComboRoundTrip_Tester.java | 4 +
.../org/apache/juneau/ComboSerializeTest_Base.java | 10 +
.../org/apache/juneau/ComboSerialize_Tester.java | 3 +
.../juneau/a/rttests/RoundTripDateTime_Test.java | 65 +-
.../juneau/a/rttests/RoundTripTest_Base.java | 43 +-
.../apache/juneau/jsonl/JsonlEdgeCases_Test.java | 125 +++
.../apache/juneau/jsonl/JsonlMediaType_Test.java | 70 ++
.../org/apache/juneau/jsonl/JsonlParser_Test.java | 130 +++
.../apache/juneau/jsonl/JsonlRoundTrip_Test.java | 158 +++
.../java/org/apache/juneau/jsonl/Jsonl_Test.java | 168 +++
.../org/apache/juneau/marshaller/Jsonl_Test.java | 97 ++
todo/jsonl_implementation.md | 708 ------------
todo/protobuf_implementation.md | 1124 --------------------
26 files changed, 1767 insertions(+), 1881 deletions(-)
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index f71f63f38e..62f62f71a4 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -13,6 +13,16 @@
Release Notes - Juneau - Version 9.2.1 - YYYY-MM-DD
+** New Features - JSONL (JSON Lines) Support
+
+ * New JsonlSerializer and JsonlParser for JSONL (JSON Lines / NDJSON) format.
+ * Media types: produces application/jsonl, accepts application/jsonl, application/x-ndjson, text/jsonl.
+ * Each line is a valid JSON value; collections and arrays serialize one element per line.
+ * Supports beans, maps, collections, scalars, ObjectSwaps, and streaming (Iterator, Iterable, Stream).
+ * New classes: JsonlSerializer, JsonlParser, Jsonl (marshaller), JsonlConfig.
+ * JSONL included in BasicUniversalConfig for REST resources.
+ * Round-trip parity with JSON for all data types; ideal for LLM datasets, streaming, and bulk pipelines.
+
** New Features - Protobuf Text Format Support
* New ProtoSerializer and ProtoParser for the human-readable Protobuf Text Format (NOT binary wire format).
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
index f43db39638..ce8c71ed16 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
@@ -33,6 +33,7 @@ import org.apache.juneau.commons.utils.*;
import org.apache.juneau.csv.annotation.*;
import org.apache.juneau.html.annotation.*;
import org.apache.juneau.json.annotation.*;
+import org.apache.juneau.jsonl.annotation.*;
import org.apache.juneau.jsonschema.annotation.*;
import org.apache.juneau.markdown.annotation.*;
import org.apache.juneau.msgpack.annotation.*;
@@ -402,6 +403,7 @@ public abstract class Context {
* <li class ='ja'>{@link HtmlConfig}
* <li class ='ja'>{@link HtmlDocConfig}
* <li class ='ja'>{@link JsonConfig}
+ * <li class ='ja'>{@link JsonlConfig}
* <li class ='ja'>{@link JsonSchemaConfig}
* <li class ='ja'>{@link MarkdownConfig}
* <li class ='ja'>{@link MsgPackConfig}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlParser.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlParser.java
new file mode 100644
index 0000000000..272a8af1c8
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlParser.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.jsonl;
+
+import java.util.Collection;
+
+import org.apache.juneau.collections.JsonList;
+import org.apache.juneau.collections.JsonMap;
+import org.apache.juneau.json.JsonParser;
+
+/**
+ * Parses JSONL (JSON Lines) input into POJO models.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Handles <c>Content-Type</c> types: <bc>application/jsonl, application/x-ndjson, text/jsonl</bc>
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * Parses JSONL input where each non-empty line is a complete JSON value. Blank lines are ignored.
+ * The conversion is as follows...
+ * <ul class='spaced-list'>
+ * <li>JSON objects (<c>{...}</c>) are converted to {@link JsonMap JsonMaps}
+ * or Java beans if a target type is provided.
+ * <li>JSON arrays (<c>[...]</c>) are converted to {@link JsonList JsonLists}.
+ * <li>JSON strings are converted to {@link String Strings}.
+ * <li>JSON numbers are converted to {@link Integer Integers}, {@link Long Longs},
+ * {@link Float Floats}, or {@link Double Doubles}.
+ * <li>JSON booleans are converted to {@link Boolean Booleans}.
+ * <li>JSON nulls are converted to <jk>null</jk>.
+ * </ul>
+ * <p>
+ * When the target type is a {@link Collection} or array, each line is parsed
+ * as one element. When the target type is a single value, only the first non-empty line is parsed.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Use the default parser to parse JSONL into a list of POJOs</jc>
+ * List<MyBean> <jv>list</jv> = JsonlParser.<jsf>DEFAULT</jsf>.parse(<jv>jsonlInput</jv>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
+ *
+ * <jc>// Create a custom parser</jc>
+ * JsonlParser <jv>parser</jv> = JsonlParser.<jsm>create</jsm>().build();
+ *
+ * <jc>// Clone an existing parser</jc>
+ * <jv>parser</jv> = JsonlParser.<jsf>DEFAULT</jsf>.copy().build();
+ * </p>
+ *
+ * <h5 class='figure'>Example input:</h5>
+ * <p class='bjson'>
+ * {"name":"Alice","age":30}
+ * {"name":"Bob","age":25}
+ * {"name":"Carol","age":35}
+ * </p>
+ *
+ * <h5 class='section'>Limitations compared to JSON</h5>
+ * <p>
+ * <ul class='spaced-list'>
+ * <li><b>No multi-line JSON input</b>: Each JSON value must be on a single line.
+ * Multi-line (pretty-printed) JSON piped as JSONL input will cause parse errors.
+ * <li><b>First-line-only for non-collection targets</b>: When parsing JSONL to a single
+ * non-collection type, only the first non-empty line is parsed; trailing lines are
+ * silently ignored.
+ * </ul>
+ *
+ * <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/JsonlBasics">JSONL Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S110" // Inheritance depth acceptable
+})
+public class JsonlParser extends JsonParser {
+
+ /** Default parser. */
+ public static final JsonlParser DEFAULT = new JsonlParser(create());
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @return A new builder.
+ */
+ public static JsonParser.Builder create() {
+ return JsonParser.create()
+ .consumes("application/jsonl,application/x-ndjson,text/jsonl")
+ .type(JsonlParser.class);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ public JsonlParser(JsonParser.Builder builder) {
+ super(builder);
+ }
+
+ @Override /* Overridden from Context */
+ public JsonlParserSession.Builder createSession() {
+ return JsonlParserSession.create(this);
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlParserSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlParserSession.java
new file mode 100644
index 0000000000..aab97f2195
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlParserSession.java
@@ -0,0 +1,133 @@
+/*
+ * 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.jsonl;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.commons.reflect.ExecutableException;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link JsonlParser}.
+ *
+ * <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/JsonlBasics">JSONL Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "unchecked", // Type erasure: elementType is ClassMeta<?>; toArray/convertToType return Object
+ "java:S110", // Inheritance depth acceptable
+ "java:S115" // Constants use UPPER_snakeCase convention
+})
+public class JsonlParserSession extends JsonParserSession {
+
+ private static final String ARG_ctx = "ctx";
+
+ /**
+ * Builder class.
+ */
+ @SuppressWarnings({
+ "unused" // ctx passed to super(); parent uses it; Builder stores for constructor chain
+ })
+ public static class Builder extends JsonParserSession.Builder {
+
+ private JsonlParser ctx;
+
+ /**
+ * Constructor
+ *
+ * @param ctx The context creating this session.
+ * <br>Cannot be <jk>null</jk>.
+ */
+ protected Builder(JsonlParser ctx) {
+ super(assertArgNotNull(ARG_ctx, ctx));
+ this.ctx = ctx;
+ }
+
+ @Override
+ public JsonlParserSession build() {
+ return new JsonlParserSession(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(JsonlParser ctx) {
+ return new Builder(assertArgNotNull(ARG_ctx, ctx));
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ protected JsonlParserSession(Builder builder) {
+ super(builder);
+ }
+
+ @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 = new JsonList(this);
+ var br = new BufferedReader(r);
+ String line;
+ while ((line = br.readLine()) != null) {
+ var trimmed = line.trim();
+ if (!trimmed.isEmpty()) {
+ 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 line
+ var br = new BufferedReader(r);
+ String line;
+ while ((line = br.readLine()) != null) {
+ var trimmed = line.trim();
+ if (!trimmed.isEmpty()) {
+ try (var linePipe = createPipe(trimmed)) {
+ return super.doParse(linePipe, type);
+ }
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlSerializer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlSerializer.java
new file mode 100644
index 0000000000..244563a172
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlSerializer.java
@@ -0,0 +1,130 @@
+/*
+ * 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.jsonl;
+
+import org.apache.juneau.json.JsonSerializer;
+import org.apache.juneau.serializer.WriterSerializer;
+import org.apache.juneau.swap.ObjectSwap;
+
+/**
+ * Serializes POJO models to JSONL (JSON Lines).
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Handles <c>Accept</c> types: <bc>application/jsonl, application/x-ndjson, text/jsonl</bc>
+ * <p>
+ * Produces <c>Content-Type</c> types: <bc>application/jsonl</bc>
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * Serializes POJOs to JSONL format where each top-level value is written as a compact JSON
+ * value on its own line. The conversion is as follows...
+ * <ul class='spaced-list'>
+ * <li>Maps and beans are converted to JSON objects, one per line.
+ * <li>Collections and arrays are converted to one JSON value per line (no wrapping brackets).
+ * <li>Strings are converted to JSON strings, one per line.
+ * <li>Numbers are converted to JSON numbers, one per line.
+ * <li>Booleans are converted to JSON booleans, one per line.
+ * <li>Nulls are converted to JSON nulls, one per line.
+ * <li>Nested objects and collections remain compact on a single line per outer element.
+ * </ul>
+ * <p>
+ * Non-JSON-primitive types are transformed through
+ * {@link ObjectSwap ObjectSwaps}.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Use the default serializer to serialize a list of POJOs</jc>
+ * String <jv>jsonl</jv> = JsonlSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>myList</jv>);
+ *
+ * <jc>// Create a custom serializer</jc>
+ * JsonlSerializer <jv>serializer</jv> = JsonlSerializer.<jsm>create</jsm>().build();
+ *
+ * <jc>// Clone an existing serializer</jc>
+ * <jv>serializer</jv> = JsonlSerializer.<jsf>DEFAULT</jsf>.copy().build();
+ *
+ * <jc>// Serialize a list of POJOs to JSONL</jc>
+ * String <jv>jsonl</jv> = <jv>serializer</jv>.serialize(<jv>myList</jv>);
+ * </p>
+ *
+ * <h5 class='figure'>Example output (List of beans):</h5>
+ * <p class='bjson'>
+ * {"name":"Alice","age":30}
+ * {"name":"Bob","age":25}
+ * {"name":"Carol","age":35}
+ * </p>
+ *
+ * <h5 class='figure'>Complex (bean with nested object and array):</h5>
+ * <p class='bjson'>
+ * {"name":"Alice","age":30,"address":{"street":"123 Main St","city":"Boston"},"tags":["a","b","c"]}
+ * {"name":"Bob","age":25,"address":{"street":"456 Oak Ave","city":"Portland"},"tags":["d","e"]}
+ * </p>
+ *
+ * <h5 class='section'>Limitations compared to JSON</h5>
+ * <p>
+ * <ul class='spaced-list'>
+ * <li><b>No pretty-printing</b>: {@link WriterSerializer.Builder#useWhitespace() useWhitespace()}
+ * is always suppressed. JSONL requires each JSON value to fit on a single line;
+ * applying intra-line pretty-printing would embed newlines and produce invalid JSONL.
+ * The setting is silently ignored regardless of its configured value.
+ * <li><b>Top-level semantics differ</b>: Collections and arrays are unwrapped — each element
+ * becomes one line. A single non-collection object produces one JSONL line.
+ * </ul>
+ *
+ * <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/JsonlBasics">JSONL Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S110" // Inheritance depth acceptable
+})
+public class JsonlSerializer extends JsonSerializer {
+
+ /** Default serializer. */
+ public static final JsonlSerializer DEFAULT = new JsonlSerializer(create());
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @return A new builder.
+ */
+ public static JsonSerializer.Builder create() {
+ return JsonSerializer.create()
+ .produces("application/jsonl")
+ .accept("application/jsonl,application/x-ndjson,text/jsonl")
+ .type(JsonlSerializer.class)
+ .useWhitespace(false);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ public JsonlSerializer(JsonSerializer.Builder builder) {
+ super(builder.useWhitespace(false));
+ }
+
+ @Override /* Overridden from Context */
+ public JsonlSerializerSession.Builder createSession() {
+ return JsonlSerializerSession.create(this);
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlSerializerSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlSerializerSession.java
new file mode 100644
index 0000000000..30d10b6c83
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/JsonlSerializerSession.java
@@ -0,0 +1,118 @@
+/*
+ * 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.jsonl;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.json.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link JsonlSerializer}.
+ *
+ * <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/JsonlBasics">JSONL Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "resource", // Resource management handled externally
+ "java:S110", // Inheritance depth acceptable
+ "java:S115" // Constants use UPPER_snakeCase convention
+})
+public class JsonlSerializerSession extends JsonSerializerSession {
+
+ private static final String ARG_ctx = "ctx";
+
+ /**
+ * Builder class.
+ */
+ @SuppressWarnings({
+ "unused" // ctx passed to super(); parent uses it; Builder stores for constructor chain
+ })
+ public static class Builder extends JsonSerializerSession.Builder {
+
+ private JsonlSerializer ctx;
+
+ /**
+ * Constructor
+ *
+ * @param ctx The context creating this session.
+ * <br>Cannot be <jk>null</jk>.
+ */
+ protected Builder(JsonlSerializer ctx) {
+ super(assertArgNotNull(ARG_ctx, ctx));
+ this.ctx = ctx;
+ }
+
+ @Override
+ public JsonlSerializerSession build() {
+ return new JsonlSerializerSession(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(JsonlSerializer ctx) {
+ return new Builder(assertArgNotNull(ARG_ctx, ctx));
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ protected JsonlSerializerSession(Builder builder) {
+ super(builder);
+ }
+
+ @Override /* Overridden from JsonSerializerSession */
+ protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
+ var w = getJsonWriter(out);
+ var cm = getClassMetaForObject(o);
+ var depth = getInitialDepth();
+
+ if (cm != null && (cm.isCollection() || cm.isArray())) {
+ var c = cm.isArray() ? toList(cm.inner(), o) : (Collection<?>) o;
+ var elementType = cm.getElementType();
+ for (Object item : c) {
+ serializeAnything(w.i(depth), item, elementType, "root", null);
+ w.w('\n');
+ }
+ } else if (cm != null && cm.isStreamable()) {
+ var elementType = cm.getElementType();
+ forEachStreamableEntry(o, cm, item -> {
+ serializeAnything(w.i(depth), item, elementType, "root", null);
+ w.w('\n');
+ });
+ } else {
+ serializeAnything(w.i(depth), o, getExpectedRootType(o), "root", null);
+ w.w('\n');
+ }
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/annotation/JsonlConfig.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/annotation/JsonlConfig.java
new file mode 100644
index 0000000000..45b835780c
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/annotation/JsonlConfig.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.jsonl.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * Annotation for specifying config properties for REST classes and methods using JSONL.
+ *
+ * <p>
+ * Used primarily for specifying bean configuration properties on REST classes and methods.
+ */
+@Target({ TYPE, METHOD })
+@Retention(RUNTIME)
+@Inherited
+@ContextApply({ JsonlConfigAnnotation.SerializerApply.class, JsonlConfigAnnotation.ParserApply.class })
+public @interface JsonlConfig {
+
+ /**
+ * 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/jsonl/annotation/JsonlConfigAnnotation.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/annotation/JsonlConfigAnnotation.java
new file mode 100644
index 0000000000..2fcc44fb2c
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/annotation/JsonlConfigAnnotation.java
@@ -0,0 +1,71 @@
+/*
+ * 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.jsonl.annotation;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.jsonl.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Utility classes and methods for the {@link JsonlConfig @JsonlConfig} annotation.
+ */
+public class JsonlConfigAnnotation {
+
+ private JsonlConfigAnnotation() {}
+
+ /**
+ * Applies {@link JsonlConfig} annotations to a {@link JsonlParser.Builder}.
+ */
+ public static class ParserApply extends AnnotationApplier<JsonlConfig, JsonParser.Builder> {
+
+ /**
+ * Constructor.
+ *
+ * @param vr The resolver for resolving values in annotations.
+ */
+ public ParserApply(VarResolverSession vr) {
+ super(JsonlConfig.class, JsonParser.Builder.class, vr);
+ }
+
+ @Override
+ public void apply(AnnotationInfo<JsonlConfig> ai, JsonParser.Builder b) {
+ // No-op: JSONL reuses JsonParser.Builder; no format-specific settings initially
+ }
+ }
+
+ /**
+ * Applies {@link JsonlConfig} annotations to a {@link JsonlSerializer.Builder}.
+ */
+ public static class SerializerApply extends AnnotationApplier<JsonlConfig, JsonSerializer.Builder> {
+
+ /**
+ * Constructor.
+ *
+ * @param vr The resolver for resolving values in annotations.
+ */
+ public SerializerApply(VarResolverSession vr) {
+ super(JsonlConfig.class, JsonSerializer.Builder.class, vr);
+ }
+
+ @Override
+ public void apply(AnnotationInfo<JsonlConfig> ai, JsonSerializer.Builder b) {
+ // No-op: JSONL reuses JsonSerializer.Builder; no format-specific settings initially
+ }
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/package-info.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/package-info.java
new file mode 100644
index 0000000000..126ee60886
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonl/package-info.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+/**
+ * JSONL (JSON Lines) marshalling support.
+ *
+ * <p>
+ * JSONL is a text format where each line is a valid JSON value, separated by newline characters.
+ * It is the standard format for LLM fine-tuning datasets, streaming AI inference, log aggregation,
+ * and bulk data pipelines.
+ *
+ * <p>
+ * This package provides {@link org.apache.juneau.jsonl.JsonlSerializer} and
+ * {@link org.apache.juneau.jsonl.JsonlParser} which extend the JSON serializer/parser to produce
+ * and consume newline-delimited JSON.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Serialize a list of beans to JSONL</jc>
+ * String <jv>jsonl</jv> = Jsonl.<jsm>of</jsm>(<jv>myList</jv>);
+ *
+ * <jc>// Parse JSONL back to a list</jc>
+ * List<MyBean> <jv>list</jv> = Jsonl.<jsm>to</jsm>(<jv>jsonl</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>
+ * </ul>
+ */
+package org.apache.juneau.jsonl;
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Jsonl.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Jsonl.java
new file mode 100644
index 0000000000..15df92a9c0
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Jsonl.java
@@ -0,0 +1,236 @@
+/*
+ * 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 java.io.*;
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.ParseException;
+import org.apache.juneau.parser.ReaderParser;
+import org.apache.juneau.serializer.SerializeException;
+import org.apache.juneau.jsonl.*;
+
+/**
+ * Pairs {@link JsonlSerializer} and {@link JsonlParser} into a single class with convenience
+ * read/write methods.
+ *
+ * <h5 class='figure'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Serialize to JSONL using instance</jc>
+ * Jsonl <jv>jsonl</jv> = <jk>new</jk> Jsonl();
+ * String <jv>out</jv> = <jv>jsonl</jv>.write(<jv>myList</jv>);
+ * List<MyBean> <jv>in</jv> = <jv>jsonl</jv>.read(<jv>out</jv>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
+ *
+ * <jc>// Serialize to JSONL using DEFAULT instance</jc>
+ * String <jv>out</jv> = Jsonl.<jsm>of</jsm>(<jv>myList</jv>);
+ * List<MyBean> <jv>in</jv> = Jsonl.<jsm>to</jsm>(<jv>out</jv>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='figure'>Example output (List of beans):</h5>
+ * <p class='bjson'>
+ * {"name":"Alice","age":30}
+ * {"name":"Bob","age":25}
+ * {"name":"Carol","age":35}
+ * </p>
+ *
+ * <h5 class='figure'>Complex (bean with nested object and array):</h5>
+ * <p class='bjson'>
+ * {"name":"Alice","age":30,"address":{"street":"123 Main St","city":"Boston"},"tags":["a","b","c"]}
+ * {"name":"Bob","age":25,"address":{"street":"456 Oak Ave","city":"Portland"},"tags":["d","e"]}
+ * </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 Jsonl extends CharMarshaller {
+
+ /**
+ * Default reusable instance.
+ */
+ public static final Jsonl DEFAULT = new Jsonl();
+
+ /**
+ * Serializes a Java object to a JSONL 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 JSONL 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 JSONL 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 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 ReaderParser.Builder#streamCharset(java.nio.charset.Charset)} property value).
+ * <li>{@link File} containing system encoded text (or charset defined by
+ * {@link ReaderParser.Builder#fileCharset(java.nio.charset.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 JSONL 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 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 ReaderParser.Builder#streamCharset(java.nio.charset.Charset)} property value).
+ * <li>{@link File} containing system encoded text (or charset defined by
+ * {@link 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 BeanSession#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 JSONL 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 JSONL 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 BeanSession#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 JsonlSerializer#DEFAULT} and {@link JsonlParser#DEFAULT}.
+ */
+ public Jsonl() {
+ this(JsonlSerializer.DEFAULT, JsonlParser.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 Jsonl(JsonlSerializer s, JsonlParser p) {
+ super(s, p);
+ }
+}
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
index 79bebd92f2..ef62eda74d 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
@@ -81,6 +81,7 @@ import org.apache.juneau.http.remote.*;
import org.apache.juneau.http.resource.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.json.*;
+import org.apache.juneau.jsonl.*;
import org.apache.juneau.markdown.*;
import org.apache.juneau.marshaller.*;
import org.apache.juneau.msgpack.*;
@@ -5557,6 +5558,7 @@ public class RestClient extends BeanContextable implements HttpClient, Closeable
serializers(
JsonSerializer.class,
Json5Serializer.class,
+ JsonlSerializer.class,
HtmlSerializer.class,
XmlSerializer.class,
UonSerializer.class,
@@ -5571,6 +5573,7 @@ public class RestClient extends BeanContextable implements HttpClient, Closeable
.parsers(
JsonParser.class,
Json5Parser.class,
+ JsonlParser.class,
XmlParser.class,
HtmlParser.class,
UonParser.class,
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
index c4b6c7f2e9..47fc894764 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
@@ -21,6 +21,7 @@ import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.html.annotation.*;
import org.apache.juneau.json.*;
+import org.apache.juneau.jsonl.*;
import org.apache.juneau.msgpack.*;
import org.apache.juneau.oapi.*;
import org.apache.juneau.plaintext.*;
@@ -141,6 +142,7 @@ import org.apache.juneau.xml.*;
HtmlSchemaDocSerializer.class,
JsonSerializer.class,
Json5Serializer.class,
+ JsonlSerializer.class,
JsonSchemaSerializer.class,
XmlDocSerializer.class,
UonSerializer.class,
@@ -160,6 +162,7 @@ import org.apache.juneau.xml.*;
parsers={
JsonParser.class,
Json5Parser.class,
+ JsonlParser.class,
XmlParser.class,
HtmlParser.class,
UonParser.class,
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
index 891ef43b76..2556f70f5f 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
@@ -99,6 +99,34 @@ public abstract class ComboRoundTripTest_Base extends TestBase {
t.testParseVerify("jsonR");
}
+ //-----------------------------------------------------------------------------------------------------------------
+ // JSONL
+ //-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a41_serializeJsonl(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testSerialize("jsonl");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a42_parseJsonl(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParse("jsonl");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a43_parseJsonlJsonEquivalency(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParseJsonEquivalency("jsonl");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a44_verifyJsonl(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParseVerify("jsonl");
+ }
+
//-----------------------------------------------------------------------------------------------------------------
// XML
//-----------------------------------------------------------------------------------------------------------------
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
index f9d6b802f8..2ccc2edb0a 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
@@ -30,6 +30,7 @@ import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.jena.*;
+import org.apache.juneau.jsonl.*;
import org.apache.juneau.msgpack.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.serializer.*;
@@ -105,6 +106,7 @@ public class ComboRoundTrip_Tester<T> {
public Builder<T> json(String value) { expected.put("json", value); return this; }
public Builder<T> jsonT(String value) { expected.put("jsonT", value); return this; }
public Builder<T> jsonR(String value) { expected.put("jsonR", value); return this; }
+ public Builder<T> jsonl(String value) { expected.put("jsonl", 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; }
@@ -172,6 +174,7 @@ public class ComboRoundTrip_Tester<T> {
serializers.put("json", create(b, Json5Serializer.DEFAULT.copy().addBeanTypes().addRootType()));
serializers.put("jsonT", create(b, JsonSerializer.create().json5().typePropertyName("t").addBeanTypes().addRootType()));
serializers.put("jsonR", create(b, Json5Serializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
+ serializers.put("jsonl", create(b, JsonlSerializer.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()));
@@ -213,6 +216,7 @@ public class ComboRoundTrip_Tester<T> {
parsers.put("json", create(b, JsonParser.DEFAULT.copy()));
parsers.put("jsonT", create(b, JsonParser.create().typePropertyName("t")));
parsers.put("jsonR", create(b, JsonParser.DEFAULT.copy()));
+ parsers.put("jsonl", create(b, JsonlParser.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-utest/src/test/java/org/apache/juneau/ComboSerializeTest_Base.java b/juneau-utest/src/test/java/org/apache/juneau/ComboSerializeTest_Base.java
index 634f3c854e..91fcfa92c7 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboSerializeTest_Base.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboSerializeTest_Base.java
@@ -54,6 +54,16 @@ public abstract class ComboSerializeTest_Base extends TestBase {
t.testSerialize("jsonR");
}
+ //-----------------------------------------------------------------------------------------------------------------
+ // JSONL
+ //-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void a41_serializeJsonl(ComboSerialize_Tester<?> t) throws Exception {
+ t.testSerialize("jsonl");
+ }
+
//-----------------------------------------------------------------------------------------------------------------
// XML
//-----------------------------------------------------------------------------------------------------------------
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java b/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java
index 8e040ed1c3..71f1a53012 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java
@@ -30,6 +30,7 @@ import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.jena.*;
+import org.apache.juneau.jsonl.*;
import org.apache.juneau.msgpack.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.uon.*;
@@ -90,6 +91,7 @@ public class ComboSerialize_Tester<T> {
public Builder<T> json(String value) { expected.put("json", value); return this; }
public Builder<T> jsonT(String value) { expected.put("jsonT", value); return this; }
public Builder<T> jsonR(String value) { expected.put("jsonR", value); return this; }
+ public Builder<T> jsonl(String value) { expected.put("jsonl", 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; }
@@ -150,6 +152,7 @@ public class ComboSerialize_Tester<T> {
serializers.put("json", create(b, Json5Serializer.DEFAULT.copy().addBeanTypes().addRootType()));
serializers.put("jsonT", create(b, Json5Serializer.create().json5().typePropertyName("t").addBeanTypes().addRootType()));
serializers.put("jsonR", create(b, Json5Serializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
+ serializers.put("jsonl", create(b, JsonlSerializer.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()));
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
index d319032a64..107ea66fc2 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
@@ -29,6 +29,7 @@ import org.apache.juneau.html.*;
import org.apache.juneau.jena.*;
import org.apache.juneau.markdown.*;
import org.apache.juneau.json.*;
+import org.apache.juneau.jsonl.*;
import org.apache.juneau.msgpack.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
@@ -57,130 +58,134 @@ class RoundTripDateTime_Test extends TestBase {
.serializer(JsonSerializer.create().json5().ws().keepNullProperties().addBeanTypes().addRootType())
.parser(JsonParser.create())
.build(),
- tester(4, "Xml - namespaces, validation, readable")
+ tester(4, "Jsonl - default")
+ .serializer(JsonlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(JsonlParser.create())
+ .build(),
+ tester(5, "Xml - namespaces, validation, readable")
.serializer(XmlSerializer.create().ns().sq().keepNullProperties().addNamespaceUrisToRoot().useWhitespace().addBeanTypes().addRootType())
.parser(XmlParser.create())
.validateXmlWhitespace()
.validateXml()
.build(),
- tester(5, "Xml - no namespaces, validation")
+ tester(6, "Xml - no namespaces, validation")
.serializer(XmlSerializer.create().sq().keepNullProperties().addBeanTypes().addRootType())
.parser(XmlParser.create())
.validateXmlWhitespace()
.build(),
- tester(6, "Html - default")
+ tester(7, "Html - default")
.serializer(HtmlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(HtmlParser.create())
.validateXmlWhitespace()
.build(),
- tester(7, "Html - readable")
+ tester(8, "Html - readable")
.serializer(HtmlSerializer.create().sq().ws().keepNullProperties().addBeanTypes().addRootType())
.parser(HtmlParser.create())
.validateXmlWhitespace()
.build(),
- tester(8, "Html - with key/value headers")
+ tester(9, "Html - with key/value headers")
.serializer(HtmlSerializer.create().addKeyValueTableHeaders().addBeanTypes().addRootType())
.parser(HtmlParser.create())
.validateXmlWhitespace()
.build(),
- tester(9, "Uon - default")
+ tester(10, "Uon - default")
.serializer(UonSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(UonParser.create())
.build(),
- tester(10, "Uon - readable")
+ tester(11, "Uon - readable")
.serializer(UonSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
.parser(UonParser.create())
.build(),
- tester(11, "Uon - encoded")
+ tester(12, "Uon - encoded")
.serializer(UonSerializer.create().encoding().keepNullProperties().addBeanTypes().addRootType())
.parser(UonParser.create().decoding())
.build(),
- tester(12, "UrlEncoding - default")
+ tester(13, "UrlEncoding - default")
.serializer(UrlEncodingSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(UrlEncodingParser.create())
.build(),
- tester(13, "UrlEncoding - readable")
+ tester(14, "UrlEncoding - readable")
.serializer(UrlEncodingSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
.parser(UrlEncodingParser.create())
.build(),
- tester(14, "UrlEncoding - expanded params")
+ tester(15, "UrlEncoding - expanded params")
.serializer(UrlEncodingSerializer.create().expandedParams().addBeanTypes().addRootType())
.parser(UrlEncodingParser.create().expandedParams())
.build(),
- tester(15, "MsgPack")
+ tester(16, "MsgPack")
.serializer(MsgPackSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(MsgPackParser.create())
.build(),
- tester(16, "RdfXml")
+ tester(17, "RdfXml")
.serializer(RdfXmlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(RdfXmlParser.create())
.build(),
- tester(17, "RdfThrift")
+ tester(18, "RdfThrift")
.serializer(RdfThriftSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(RdfThriftParser.create())
.build(),
- tester(18, "RdfProto")
+ tester(19, "RdfProto")
.serializer(RdfProtoSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(RdfProtoParser.create())
.build(),
- tester(19, "RdfXmlAbbrev")
+ tester(20, "RdfXmlAbbrev")
.serializer(RdfXmlAbbrevSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(RdfXmlParser.create())
.build(),
- tester(20, "RdfTurtle")
+ tester(21, "RdfTurtle")
.serializer(TurtleSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(TurtleParser.create())
.build(),
- tester(21, "RdfN3")
+ tester(22, "RdfN3")
.serializer(N3Serializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(N3Parser.create())
.build(),
- tester(22, "RdfNtriple")
+ tester(23, "RdfNtriple")
.serializer(NTripleSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(NTripleParser.create())
.build(),
- tester(23, "RdfNquads")
+ tester(24, "RdfNquads")
.serializer(NQuadsSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(NQuadsParser.create())
.build(),
- tester(24, "RdfTrig")
+ tester(25, "RdfTrig")
.serializer(TriGSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(TriGParser.create())
.build(),
- tester(25, "RdfJsonLd")
+ tester(26, "RdfJsonLd")
.serializer(JsonLdSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(JsonLdParser.create())
.build(),
- tester(26, "RdfJson")
+ tester(27, "RdfJson")
.serializer(RdfJsonSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(RdfJsonParser.create())
.build(),
- tester(27, "RdfTriX")
+ tester(28, "RdfTriX")
.serializer(TriXSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(TriXParser.create())
.build(),
- tester(28, "Json schema")
+ tester(29, "Json schema")
.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.returnOriginalObject()
.build(),
- tester(29, "Yaml - default")
+ tester(30, "Yaml - default")
.serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(YamlParser.create())
.build(),
- tester(30, "Toml - default")
+ tester(31, "Toml - default")
.serializer(TomlSerializer.create())
.parser(TomlParser.create())
.build(),
- tester(31, "Csv - default")
+ tester(32, "Csv - default")
.serializer(CsvSerializer.create().keepNullProperties())
.skipIf(o -> o == null || (o.getClass().isArray() && o.getClass().getComponentType().isPrimitive()))
.returnOriginalObject()
.build(),
- tester(32, "Markdown - default")
+ tester(33, "Markdown - default")
.serializer(MarkdownSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(MarkdownParser.create())
.build(),
- tester(33, "Proto - default")
+ tester(34, "Proto - default")
.serializer(ProtoSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(ProtoParser.create())
.build(),
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
index 5aaf97ee67..9bdc292011 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
@@ -23,6 +23,7 @@ import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.jena.*;
import org.apache.juneau.json.*;
+import org.apache.juneau.jsonl.*;
import org.apache.juneau.msgpack.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
@@ -52,81 +53,85 @@ public abstract class RoundTripTest_Base extends TestBase {
.serializer(JsonSerializer.create().json5().ws().keepNullProperties().addBeanTypes().addRootType())
.parser(JsonParser.create())
.build(),
- tester(4, "Xml - namespaces, validation, readable")
+ tester(4, "Jsonl - default")
+ .serializer(JsonlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(JsonlParser.create())
+ .build(),
+ tester(5, "Xml - namespaces, validation, readable")
.serializer(XmlSerializer.create().ns().sq().keepNullProperties().addNamespaceUrisToRoot().useWhitespace().addBeanTypes().addRootType())
.parser(XmlParser.create())
.validateXmlWhitespace()
.validateXml()
.build(),
- tester(5, "Xml - no namespaces, validation")
+ tester(6, "Xml - no namespaces, validation")
.serializer(XmlSerializer.create().sq().keepNullProperties().addBeanTypes().addRootType())
.parser(XmlParser.create())
.validateXmlWhitespace()
.build(),
- tester(6, "Html - default")
+ tester(7, "Html - default")
.serializer(HtmlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(HtmlParser.create())
.validateXmlWhitespace()
.build(),
- tester(7, "Html - readable")
+ tester(8, "Html - readable")
.serializer(HtmlSerializer.create().sq().ws().keepNullProperties().addBeanTypes().addRootType())
.parser(HtmlParser.create())
.validateXmlWhitespace()
.build(),
- tester(8, "Html - with key/value headers")
+ tester(9, "Html - with key/value headers")
.serializer(HtmlSerializer.create().addKeyValueTableHeaders().addBeanTypes().addRootType())
.parser(HtmlParser.create())
.validateXmlWhitespace()
.build(),
- tester(9, "Uon - default")
+ tester(10, "Uon - default")
.serializer(UonSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(UonParser.create())
.build(),
- tester(10, "Uon - readable")
+ tester(11, "Uon - readable")
.serializer(UonSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
.parser(UonParser.create())
.build(),
- tester(11, "Uon - encoded")
+ tester(12, "Uon - encoded")
.serializer(UonSerializer.create().encoding().keepNullProperties().addBeanTypes().addRootType())
.parser(UonParser.create().decoding())
.build(),
- tester(12, "UrlEncoding - default")
+ tester(13, "UrlEncoding - default")
.serializer(UrlEncodingSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(UrlEncodingParser.create())
.build(),
- tester(13, "UrlEncoding - readable")
+ tester(14, "UrlEncoding - readable")
.serializer(UrlEncodingSerializer.create().ws().keepNullProperties().addBeanTypes().addRootType())
.parser(UrlEncodingParser.create())
.build(),
- tester(14, "UrlEncoding - expanded params")
+ tester(15, "UrlEncoding - expanded params")
.serializer(UrlEncodingSerializer.create().expandedParams().addBeanTypes().addRootType())
.parser(UrlEncodingParser.create().expandedParams())
.build(),
- tester(15, "MsgPack")
+ tester(16, "MsgPack")
.serializer(MsgPackSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(MsgPackParser.create())
.build(),
- tester(16, "RdfXml")
+ tester(17, "RdfXml")
.serializer(RdfXmlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(RdfXmlParser.create())
.build(),
- tester(17, "RdfThrift")
+ tester(18, "RdfThrift")
.serializer(RdfThriftSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(RdfThriftParser.create())
.build(),
- tester(18, "RdfProto")
+ tester(19, "RdfProto")
.serializer(RdfProtoSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(RdfProtoParser.create())
.build(),
- tester(19, "Json schema")
+ tester(20, "Json schema")
.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.returnOriginalObject()
.build(),
- tester(20, "Yaml - default")
+ tester(21, "Yaml - default")
.serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(YamlParser.create())
.build(),
- tester(21, "Csv - default")
+ tester(22, "Csv - default")
.serializer(CsvSerializer.create().keepNullProperties())
// CSV serialization is validated here without parsing (returnOriginalObject), analogous
// to the JSON schema tester. Full CSV round-trip tests are in CsvParser_Test.
@@ -134,7 +139,7 @@ public abstract class RoundTripTest_Base extends TestBase {
.skipIf(o -> !isCsvSerializableInput(o))
.returnOriginalObject()
.build(),
- tester(22, "Markdown - default")
+ tester(23, "Markdown - default")
.serializer(MarkdownSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(MarkdownParser.create())
.build(),
diff --git a/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlEdgeCases_Test.java b/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlEdgeCases_Test.java
new file mode 100644
index 0000000000..f1acc58166
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlEdgeCases_Test.java
@@ -0,0 +1,125 @@
+/*
+ * 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.jsonl;
+
+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 java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.marshaller.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Edge case tests for {@link JsonlSerializer} and {@link JsonlParser}.
+ */
+@SuppressWarnings({
+ "unchecked" // Parser returns Object; cast to List<JsonMap>/List<Object> in tests
+})
+class JsonlEdgeCases_Test extends TestBase {
+
+ @Test
+ void a01_veryLargeCollection() throws Exception {
+ var a = IntStream.range(0, 10_000)
+ .mapToObj(i -> JsonMap.of("id", i, "name", "item" + i))
+ .toList();
+ var jsonl = Jsonl.of(a);
+ var lines = jsonl.split("\n");
+ assertEquals(10_000, lines.length);
+ var b = (List<JsonMap>) Jsonl.to(jsonl, List.class, JsonMap.class);
+ assertBean(b, "0{id,name},9999{id,name}", "{0,item0},{9999,item9999}");
+ }
+
+ @Test
+ void a02_unicodeContent() throws Exception {
+ var a = list(JsonMap.of("text", "Hello 世界 café \uD83D\uDE00"));
+ var jsonl = Jsonl.of(a);
+ var b = (List<JsonMap>) Jsonl.to(jsonl, List.class, JsonMap.class);
+ assertBean(b, "0{text}", "{Hello 世界 café \uD83D\uDE00}");
+ }
+
+ @Test
+ void a03_specialCharactersInStrings() throws Exception {
+ var a = list(JsonMap.of("s", "a\nb\tc\"d\\e"));
+ var jsonl = Jsonl.of(a);
+ var b = (List<JsonMap>) Jsonl.to(jsonl, List.class, JsonMap.class);
+ assertBean(b, "0{s}", "{a\nb\tc\"d\\e}");
+ }
+
+ @Test
+ void a04_deeplyNestedObjects() throws Exception {
+ var inner = JsonMap.of("depth", 10);
+ for (var i = 9; i >= 1; i--) {
+ inner = JsonMap.of("depth", i, "child", inner);
+ }
+ var a = list(inner);
+ var jsonl = Jsonl.of(a);
+ var b = (List<JsonMap>) Jsonl.to(jsonl, List.class, JsonMap.class);
+ assertEquals(1, b.size());
+ var c = b.get(0);
+ for (var i = 1; i <= 10; i++) {
+ assertEquals(i, c.getInt("depth"));
+ c = c.get("child", JsonMap.class);
+ if (i < 10)
+ assertNotNull(c);
+ }
+ }
+
+ @Test
+ void a05_emptyObjects() throws Exception {
+ var a = list(JsonMap.of(), JsonList.of());
+ var jsonl = Jsonl.of(a);
+ var b = (List<Object>) Jsonl.to(jsonl, List.class, Object.class);
+ assertEquals(2, b.size());
+ assertTrue(b.get(0) instanceof JsonMap);
+ assertEquals(0, ((JsonMap) b.get(0)).size());
+ assertTrue(b.get(1) instanceof JsonList);
+ assertEquals(0, ((JsonList) b.get(1)).size());
+ }
+
+ @Test
+ void a06_mixedLineEndings() throws Exception {
+ var jsonl = "{\"a\":1}\r\n{\"b\":2}";
+ var b = (List<JsonMap>) Jsonl.to(jsonl, List.class, JsonMap.class);
+ assertBean(b, "0{a},1{b}", "{1},{2}");
+ }
+
+ @Test
+ void a07_whitespaceOnlyLines() throws Exception {
+ var jsonl = "{\"a\":1}\n \n\t\n{\"b\":2}";
+ var b = (List<JsonMap>) Jsonl.to(jsonl, List.class, JsonMap.class);
+ assertBean(b, "0{a},1{b}", "{1},{2}");
+ }
+
+ @Test
+ void a08_booleanAndNullLines() throws Exception {
+ var jsonl = "true\nfalse\nnull";
+ var b = (List<Object>) Jsonl.to(jsonl, List.class, Object.class);
+ assertBean(b, "0,1,2", "true,false,<null>");
+ }
+
+ @Test
+ void a09_numericLines() throws Exception {
+ var jsonl = "1\n2.5\n-3\n1e10";
+ var b = (List<Object>) Jsonl.to(jsonl, List.class, Object.class);
+ assertBean(b, "0,1,2,3", "1,2.5,-3,1.0E10");
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlMediaType_Test.java b/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlMediaType_Test.java
new file mode 100644
index 0000000000..3e238c21eb
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlMediaType_Test.java
@@ -0,0 +1,70 @@
+/*
+ * 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.jsonl;
+
+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.collections.*;
+import org.apache.juneau.marshaller.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for JSONL media type configuration.
+ */
+@SuppressWarnings({
+ "unchecked" // Parser returns Object; cast to List<JsonMap> in tests
+})
+class JsonlMediaType_Test {
+
+ @Test
+ void a01_producesCorrectMediaType() {
+ var ct = JsonlSerializer.DEFAULT.getResponseContentType();
+ assertEquals("application", ct.getType());
+ assertEquals("jsonl", ct.getSubType());
+ }
+
+ @Test
+ void a02_acceptsAllMediaTypes() {
+ var types = new ArrayList<String>();
+ JsonlSerializer.DEFAULT.forEachAcceptMediaType(mt -> types.add(mt.getType() + "/" + mt.getSubType()));
+ assertTrue(types.stream().anyMatch(t -> "application/jsonl".equals(t) || t.contains("jsonl")), "Expected application/jsonl: " + types);
+ assertTrue(types.stream().anyMatch(t -> "application/x-ndjson".equals(t) || t.contains("ndjson")), "Expected application/x-ndjson: " + types);
+ assertTrue(types.stream().anyMatch("text/jsonl"::equals), "Expected text/jsonl: " + types);
+ }
+
+ @Test
+ void a03_consumesAllMediaTypes() {
+ var types = JsonlParser.DEFAULT.getMediaTypes().stream()
+ .map(mt -> mt.getType() + "/" + mt.getSubType())
+ .toList();
+ assertTrue(types.stream().anyMatch(t -> t.contains("jsonl")), "Expected jsonl types: " + types);
+ assertTrue(types.stream().anyMatch(t -> t.contains("ndjson")), "Expected ndjson: " + types);
+ }
+
+ @Test
+ void a04_contentNegotiation() throws Exception {
+ var a = list(JsonMap.of("k", "v"));
+ var jsonl = Jsonl.of(a);
+ assertNotNull(jsonl);
+ var b = (List<JsonMap>) Jsonl.to(jsonl, List.class, JsonMap.class);
+ assertBean(b, "0{k}", "{v}");
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlParser_Test.java b/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlParser_Test.java
new file mode 100644
index 0000000000..8a561219a6
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlParser_Test.java
@@ -0,0 +1,130 @@
+/*
+ * 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.jsonl;
+
+import static org.apache.juneau.TestUtils.*;
+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.parser.*;
+import org.apache.juneau.swaps.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.marshaller.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link org.apache.juneau.jsonl.JsonlParser}.
+ */
+@SuppressWarnings({
+ "unchecked" // Parser returns Object; cast to List<Person>/List<JsonMap>/List<String> in tests
+})
+class JsonlParser_Test extends TestBase {
+
+ @Bean(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_parseToListOfBeans() throws Exception {
+ var jsonl = "{\"name\":\"Alice\",\"age\":30}\n{\"name\":\"Bob\",\"age\":25}\n{\"name\":\"Carol\",\"age\":35}";
+ var list = (List<Person>) Jsonl.to(jsonl, List.class, Person.class);
+ assertBean(list, "0{name,age},1{name,age},2{name,age}", "{Alice,30},{Bob,25},{Carol,35}");
+ }
+
+ @Test
+ void a02_parseToArray() throws Exception {
+ var jsonl = "{\"name\":\"Alice\",\"age\":30}\n{\"name\":\"Bob\",\"age\":25}";
+ var arr = Jsonl.to(jsonl, Person[].class);
+ assertBean(arr, "0{name,age},1{name,age}", "{Alice,30},{Bob,25}");
+ }
+
+ @Test
+ void a03_parseSingleLine() throws Exception {
+ var jsonl = "{\"name\":\"Alice\",\"age\":30}";
+ var p = Jsonl.to(jsonl, Person.class);
+ assertBean(p, "name,age", "Alice,30");
+ }
+
+ @Test
+ void a04_parseWithEmptyLines() throws Exception {
+ var jsonl = "{\"name\":\"Alice\"}\n\n\n{\"name\":\"Bob\"}";
+ var list = (List<Person>) Jsonl.to(jsonl, List.class, Person.class);
+ assertBean(list, "0{name},1{name}", "{Alice},{Bob}");
+ }
+
+ @Test
+ void a05_parseToListOfMaps() throws Exception {
+ var jsonl = "{\"a\":1,\"b\":2}\n{\"x\":\"y\"}";
+ var list = (List<JsonMap>) Jsonl.to(jsonl, List.class, JsonMap.class);
+ assertBean(list, "0{a,b},1{x}", "{1,2},{y}");
+ }
+
+ @Test
+ void a06_parseToListOfStrings() throws Exception {
+ var jsonl = "\"foo\"\n\"bar\"\n\"baz\"";
+ var list = (List<String>) Jsonl.to(jsonl, List.class, String.class);
+ assertEquals(list("foo", "bar", "baz"), list);
+ }
+
+ @Test
+ void a07_parseEmptyInput() throws Exception {
+ var list = (List<?>) Jsonl.to("", List.class, Person.class);
+ assertNotNull(list);
+ assertTrue(list.isEmpty());
+ }
+
+ @Test
+ void a08_parseWithTrailingNewline() throws Exception {
+ var jsonl = "{\"name\":\"Alice\"}\n";
+ var list = (List<Person>) Jsonl.to(jsonl, List.class, Person.class);
+ assertBean(list, "0{name}", "{Alice}");
+ }
+
+ @Test
+ void a09_parseNestedObjects() throws Exception {
+ var jsonl = "{\"name\":\"Alice\",\"addr\":{\"city\":\"Boston\",\"zip\":\"02101\"}}";
+ var list = (List<JsonMap>) Jsonl.to(jsonl, List.class, JsonMap.class);
+ assertBean(list, "0{name,addr{city,zip}}", "{Alice,{Boston,02101}}");
+ }
+
+ @Test
+ void a10_parseWithSwaps() throws Exception {
+ var p = (JsonlParser) JsonlParser.create().swaps(ByteArraySwap.Base64.class).build();
+ var jsonl = "{\"data\":\"AQID\"}\n{\"data\":\"BAUG\"}";
+ var list = (List<JsonMap>) p.parse(jsonl, List.class, JsonMap.class);
+ assertBean(list, "0{data},1{data}", "{AQID},{BAUG}");
+ }
+
+ @Test
+ void a11_parseMalformedLine() {
+ assertThrowsWithMessage(ParseException.class, "invalid", () ->
+ Jsonl.to("{\"a\":1}\n{invalid json}", List.class, JsonMap.class));
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlRoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlRoundTrip_Test.java
new file mode 100644
index 0000000000..2561aaa74e
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/jsonl/JsonlRoundTrip_Test.java
@@ -0,0 +1,158 @@
+/*
+ * 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.jsonl;
+
+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.annotation.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.marshaller.*;
+import org.apache.juneau.swaps.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Round-trip tests for {@link JsonlSerializer} and {@link JsonlParser}.
+ */
+@SuppressWarnings({
+ "unchecked" // Parser returns Object; cast to List<Person>/List<ComplexPerson>/List<JsonMap>/List<Object>/List<String> in tests
+})
+class JsonlRoundTrip_Test extends TestBase {
+
+ @Bean(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;
+ }
+ }
+
+ @Bean(properties = "name,address,tags")
+ public static class ComplexPerson {
+ public String name;
+ public Address address;
+ public List<String> tags;
+
+ public ComplexPerson() {}
+ public ComplexPerson(String name, Address address, List<String> tags) {
+ this.name = name;
+ this.address = address;
+ this.tags = tags;
+ }
+ }
+
+ @Bean(properties = "street,city")
+ public static class Address {
+ public String street;
+ public String city;
+
+ public Address() {}
+ public Address(String street, String city) {
+ this.street = street;
+ this.city = city;
+ }
+ }
+
+ @Test
+ void a01_roundTripBeanCollection() throws Exception {
+ var a = list(
+ new Person("Alice", 30),
+ new Person("Bob", 25),
+ new Person("Carol", 35)
+ );
+ var jsonl = Jsonl.of(a);
+ var b = (List<Person>) Jsonl.to(jsonl, List.class, Person.class);
+ assertBean(b, "0{name,age},1{name,age},2{name,age}", "{Alice,30},{Bob,25},{Carol,35}");
+ }
+
+ @Test
+ void a02_roundTripBeanArray() throws Exception {
+ var a = new Person[]{new Person("Alice", 30), new Person("Bob", 25)};
+ var jsonl = Jsonl.of(a);
+ var b = Jsonl.to(jsonl, Person[].class);
+ assertBean(b, "0{name,age},1{name,age}", "{Alice,30},{Bob,25}");
+ }
+
+ @Test
+ void a03_roundTripComplexBeans() throws Exception {
+ var a = list(
+ new ComplexPerson("Alice", new Address("123 Main", "Boston"), list("a", "b", "c")),
+ new ComplexPerson("Bob", new Address("456 Oak", "Portland"), list("d", "e"))
+ );
+ var jsonl = Jsonl.of(a);
+ var b = (List<ComplexPerson>) Jsonl.to(jsonl, List.class, ComplexPerson.class);
+ assertBean(b, "0{name,address{street,city},tags},1{name,address{street,city},tags}",
+ "{Alice,{123 Main,Boston},[a,b,c]},{Bob,{456 Oak,Portland},[d,e]}");
+ }
+
+ @Test
+ void a04_roundTripWithNulls() throws Exception {
+ var a = list("a", null, "c");
+ var jsonl = Jsonl.of(a);
+ var b = (List<String>) Jsonl.to(jsonl, List.class, String.class);
+ assertBean(b, "0,1,2", "a,<null>,c");
+ }
+
+ @Test
+ void a05_roundTripMixedTypes() throws Exception {
+ var a = list(1, "two", true, null, 3.5);
+ var jsonl = Jsonl.of(a);
+ var b = (List<Object>) Jsonl.to(jsonl, List.class, Object.class);
+ assertBean(b, "0,1,2,3,4", "1,two,true,<null>,3.5");
+ }
+
+ @Test
+ void a06_roundTripWithSwaps() throws Exception {
+ var s = (JsonlSerializer) JsonlSerializer.create().swaps(ByteArraySwap.Base64.class).build();
+ var p = (JsonlParser) JsonlParser.create().swaps(ByteArraySwap.Base64.class).build();
+ var m = new Jsonl(s, p);
+ var a = list(
+ JsonMap.of("name", "Alice", "data", new byte[]{1, 2, 3}),
+ JsonMap.of("name", "Bob", "data", new byte[]{4, 5, 6})
+ );
+ var jsonl = m.write(a);
+ var b = (List<JsonMap>) m.read(jsonl, List.class, JsonMap.class);
+ assertBean(b, "0{name},1{name}", "{Alice},{Bob}");
+ assertTrue(jsonl.contains("AQID"));
+ assertTrue(jsonl.contains("BAUG"));
+ }
+
+ @Test
+ void a07_roundTripSingleObject() throws Exception {
+ var a = new Person("Alice", 30);
+ var jsonl = Jsonl.of(a);
+ var b = Jsonl.to(jsonl, Person.class);
+ assertBean(b, "name,age", "Alice,30");
+ }
+
+ @Test
+ void a08_roundTripEmptyCollection() throws Exception {
+ var a = list();
+ var jsonl = Jsonl.of(a);
+ var b = (List<?>) Jsonl.to(jsonl, List.class, Person.class);
+ assertNotNull(b);
+ assertTrue(b.isEmpty());
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/jsonl/Jsonl_Test.java b/juneau-utest/src/test/java/org/apache/juneau/jsonl/Jsonl_Test.java
new file mode 100644
index 0000000000..e8fb690465
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/jsonl/Jsonl_Test.java
@@ -0,0 +1,168 @@
+/*
+ * 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.jsonl;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.marshaller.*;
+import org.apache.juneau.swaps.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link JsonlSerializer}.
+ */
+class Jsonl_Test extends TestBase {
+
+ @Bean(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_serializeCollectionOfBeans() throws Exception {
+ var list = list(
+ new Person("Alice", 30),
+ new Person("Bob", 25),
+ new Person("Carol", 35)
+ );
+ var jsonl = Jsonl.of(list);
+ var lines = jsonl.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(jsonl.contains("["));
+ assertFalse(jsonl.contains("]"));
+ }
+
+ @Test
+ void a02_serializeArray() throws Exception {
+ var arr = new Person[]{new Person("Alice", 30), new Person("Bob", 25)};
+ var jsonl = Jsonl.of(arr);
+ var lines = jsonl.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 jsonl = Jsonl.of(p);
+ assertEquals(1, jsonl.split("\n").length);
+ assertTrue(jsonl.contains("\"name\":\"Alice\""));
+ assertTrue(jsonl.contains("\"age\":30"));
+ }
+
+ @Test
+ void a04_serializeCollectionOfMaps() throws Exception {
+ var list = list(
+ JsonMap.of("a", 1, "b", 2),
+ JsonMap.of("x", "y")
+ );
+ var jsonl = Jsonl.of(list);
+ var lines = jsonl.split("\n");
+ assertEquals(2, lines.length);
+ assertTrue(lines[0].contains("\"a\":1"));
+ assertTrue(lines[1].contains("\"x\":\"y\""));
+ }
+
+ @Test
+ void a05_serializeCollectionOfStrings() throws Exception {
+ var list = list("foo", "bar", "baz");
+ var jsonl = Jsonl.of(list);
+ var lines = jsonl.split("\n");
+ assertEquals(3, lines.length);
+ assertTrue(lines[0].contains("\"foo\""));
+ assertTrue(lines[1].contains("\"bar\""));
+ assertTrue(lines[2].contains("\"baz\""));
+ }
+
+ @Test
+ void a06_serializeCollectionOfNumbers() throws Exception {
+ var list = list(1, 2, 3);
+ var jsonl = Jsonl.of(list);
+ var lines = jsonl.split("\n");
+ assertEquals(3, lines.length);
+ assertEquals("1", lines[0]);
+ assertEquals("2", lines[1]);
+ assertEquals("3", lines[2]);
+ }
+
+ @Test
+ void a07_serializeEmptyCollection() throws Exception {
+ var jsonl = Jsonl.of(list());
+ assertEquals("", jsonl.trim());
+ }
+
+ @Test
+ void a08_serializeNullValues() throws Exception {
+ var list = list("a", null, "c");
+ var jsonl = Jsonl.of(list);
+ var lines = jsonl.split("\n");
+ assertEquals(3, lines.length);
+ assertEquals("null", lines[1]);
+ }
+
+ @Test
+ void a09_serializeNestedObjects() throws Exception {
+ var outer = JsonMap.of("name", "Alice", "inner", JsonMap.of("x", 1, "y", 2));
+ var jsonl = Jsonl.of(outer);
+ assertTrue(jsonl.contains("\"inner\":{\"x\":1,\"y\":2}"));
+ }
+
+ @Test
+ void a10_noTrailingComma() throws Exception {
+ var list = list(new Person("A", 1), new Person("B", 2));
+ var jsonl = Jsonl.of(list);
+ assertFalse(jsonl.contains(",,"));
+ assertFalse(jsonl.contains("\n,"));
+ }
+
+ @Test
+ void a11_noWhitespaceBetweenLines() throws Exception {
+ var list = list(new Person("A", 1), new Person("B", 2));
+ var jsonl = Jsonl.of(list);
+ var lines = jsonl.split("\n");
+ for (var line : lines) {
+ if (!line.isEmpty()) {
+ assertFalse(line.startsWith(" "), "Line should not start with space: " + line);
+ assertFalse(line.contains("\n"), "Line should not contain newline");
+ }
+ }
+ }
+
+ @Test
+ void a12_serializeWithSwaps() throws Exception {
+ var s = (JsonlSerializer) JsonlSerializer.create().swaps(ByteArraySwap.Base64.class).build();
+ var list = list(JsonMap.of("data", new byte[]{1, 2, 3}));
+ var jsonl = s.serialize(list);
+ assertTrue(jsonl.contains("AQID"));
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/marshaller/Jsonl_Test.java b/juneau-utest/src/test/java/org/apache/juneau/marshaller/Jsonl_Test.java
new file mode 100644
index 0000000000..fbc6eae7ec
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/marshaller/Jsonl_Test.java
@@ -0,0 +1,97 @@
+/*
+ * 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.collections.*;
+import org.junit.jupiter.api.*;
+
+@SuppressWarnings({
+ "unchecked" // Parser returns Object; cast to Map/List<JsonMap> in tests
+})
+class Jsonl_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\"", Jsonl.of(a).trim());
+ assertEquals("\"foo\"", Jsonl.of(a, stringWriter()).toString().trim());
+ assertTrue(Jsonl.of(b).contains("\"foo\":\"bar\""));
+ var sw = new StringWriter();
+ Jsonl.of(b, sw);
+ assertTrue(sw.toString().contains("\"foo\":\"bar\""));
+ var jsonl = Jsonl.of(c);
+ assertTrue(jsonl.contains("\"a\":1"));
+ assertTrue(jsonl.contains("\"b\":2"));
+ assertEquals(2, jsonl.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", Jsonl.to(a, String.class));
+ assertEquals("foo", Jsonl.to(stringReader(a), String.class));
+ var m = (Map<String,String>) Jsonl.to(b, Map.class, String.class, String.class);
+ assertEquals("bar", m.get("foo"));
+ m = (Map<String,String>) Jsonl.to(stringReader(b), Map.class, String.class, String.class);
+ assertEquals("bar", m.get("foo"));
+ var list = (List<JsonMap>) Jsonl.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 jsonl = Jsonl.of(a);
+ var b = (List<JsonMap>) Jsonl.to(jsonl, 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 jsonl = Jsonl.DEFAULT.write(a);
+ assertTrue(jsonl.contains("\"k\":\"v\""));
+ var b = (List<JsonMap>) Jsonl.DEFAULT.read(jsonl, 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/todo/jsonl_implementation.md b/todo/jsonl_implementation.md
deleted file mode 100644
index 0516df0384..0000000000
--- a/todo/jsonl_implementation.md
+++ /dev/null
@@ -1,708 +0,0 @@
-# JSONL (JSON Lines) Implementation Plan for Apache Juneau
-
-## Overview
-
-JSONL (JSON Lines, also called Newline-Delimited JSON / NDJSON) is a text format where each line is a valid JSON value, separated by newline characters (`\n`). It is the standard format for LLM fine-tuning datasets, streaming AI inference, log aggregation, and bulk data pipelines.
-
-This plan covers implementing JSONL serializer and parser support in Juneau by extending the existing `JsonSerializer`/`JsonParser` infrastructure, following the `Json5Serializer`/`Json5Parser` extension pattern.
-
-## Specification
-
-- **Media types**: `application/jsonl`, `application/x-ndjson`, `text/jsonl`
-- **RFC reference**: https://jsonlines.org/
-- **Key rules**:
- - Each line is a valid JSON value (typically an object)
- - Lines are separated by `\n` (newline)
- - No trailing comma or wrapping array brackets
- - Empty lines are ignored
- - UTF-8 encoding
-
-## Architecture
-
-JSONL builds directly on the existing JSON infrastructure. The key difference is at the collection/array level:
-
-- **Standard JSON**: `[{"a":1},{"a":2},{"a":3}]`
-- **JSONL**: `{"a":1}\n{"a":2}\n{"a":3}\n`
-
-### Serializer Approach
-
-`JsonlSerializer` extends `JsonSerializer`. It overrides session creation to use `JsonlSerializerSession`, which overrides `doSerialize()` to:
-- If the object is a collection/array, serialize each element as a separate JSON line (no wrapping `[]`, no commas)
-- If the object is a single value, serialize it as a single JSON line
-- Each line uses the existing `serializeAnything()` method from `JsonSerializerSession`
-
-### Parser Approach
-
-`JsonlParser` extends `JsonParser`. It overrides session creation to use `JsonlParserSession`, which overrides `doParse()` to:
-- Read input line by line
-- Parse each non-empty line as a JSON value using the existing `parseAnything()` method from `JsonParserSession`
-- Collect results into a `List`/`Collection` or array depending on the target type
-- If the target type is a single object (not collection/array), parse only the first line
-
----
-
-## Round-Trip Parity with JSON
-
-JSONL supports **all the same data structures** as JSON:
-
-- Maps and beans → serialized as JSON objects, one per line
-- Collections and arrays → each element becomes one JSONL line
-- Strings → JSON string literals, one per line
-- Numbers → JSON number literals, one per line
-- Booleans → `true` / `false`, one per line
-- Null → `null`, one per line
-- Nested objects and collections → stay compact on a single line per outer element
-- Typed round-trip via `_type` attribute (using `addBeanTypes()` / `addRootType()`)
-- ObjectSwaps, date/time types (ISO 8601), Iterators, and Streams
-
-The only structural difference from JSON is at the **top-level container**: JSONL treats the top level as a sequence of values (one per line) rather than a single value. Collections and arrays round-trip naturally. Single non-collection objects produce a single JSONL line and round-trip identically to JSON.
-
-### Limitations compared to JSON
-
-Following the pattern established by `YamlSerializer` (`<h5 class='section'>Limitations compared to JSON</h5>`):
-
-1. **No pretty-printing**: `useWhitespace()` is always suppressed in `JsonlSerializerSession`. JSONL requires each JSON value to fit on a single line; applying intra-line pretty-printing would embed newlines and produce invalid JSONL. The setting is silently ignored regardless of its configured value.
-
-2. **No multi-line JSON input**: The parser reads one JSON value per line. Multi-line (pretty-printed) JSON piped as JSONL input will cause parse errors.
-
-3. **Top-level semantics differ**: JSON treats the top-level as a single value. JSONL treats the top level as a stream of values. When parsing JSONL to a single non-collection type, only the first line is parsed; any trailing lines are silently ignored.
-
----
-
-## Files to Create
-
-All new source files go in the `juneau-core/juneau-marshall` module under `src/main/java/org/apache/juneau/jsonl/`.
-
-### 1. `JsonlSerializer.java`
-
-Extends `JsonSerializer`. Follows the `Json5Serializer` pattern.
-
-```java
-package org.apache.juneau.jsonl;
-
-public class JsonlSerializer extends JsonSerializer {
-
- public static final JsonlSerializer DEFAULT = new JsonlSerializer(create());
-
- public static JsonSerializer.Builder create() {
- return JsonSerializer.create()
- .produces("application/jsonl")
- .accept("application/jsonl,application/x-ndjson,text/jsonl")
- .type(JsonlSerializer.class);
- }
-
- public JsonlSerializer(JsonSerializer.Builder builder) {
- super(builder);
- }
-
- @Override
- public JsonlSerializerSession.Builder createSession() {
- return JsonlSerializerSession.create(this);
- }
-}
-```
-
-Key design decisions:
-- Reuses `JsonSerializer.Builder` (no separate builder needed, same as `Json5Serializer`)
-- Overrides `createSession()` to return a `JsonlSerializerSession` that changes serialization behavior at the collection level
-- Provides `DEFAULT` static instance
-
-### 2. `JsonlSerializerSession.java`
-
-Extends `JsonSerializerSession`. Overrides `doSerialize()` to emit one JSON object per line.
-
-```java
-package org.apache.juneau.jsonl;
-
-public class JsonlSerializerSession extends JsonSerializerSession {
-
- @Override
- protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
- JsonWriter w = getJsonWriter(out);
- ClassMeta<?> cm = getClassMetaForObject(o);
-
- if (cm != null && (cm.isCollection() || cm.isArray())) {
- Collection<?> c = cm.isArray() ? toList(cm.inner(), o) : (Collection<?>) o;
- ClassMeta<?> elementType = cm.getElementType();
- for (Object item : c) {
- serializeAnything(w.i(getInitialDepth()), item, elementType, "root", null);
- w.w('\n');
- }
- } else {
- serializeAnything(w.i(getInitialDepth()), o, getExpectedRootType(o), "root", null);
- w.w('\n');
- }
- }
-}
-```
-
-Key behavior:
-- Collections and arrays: each element is serialized as a compact JSON line (no whitespace between lines, no wrapping brackets)
-- Single objects: serialized as a single JSON line
-- Each line terminated with `\n`
-- Reuses `serializeAnything()` from `JsonSerializerSession` for all JSON serialization logic
-
-### 3. `JsonlParser.java`
-
-Extends `JsonParser`. Follows the `Json5Parser` pattern.
-
-```java
-package org.apache.juneau.jsonl;
-
-public class JsonlParser extends JsonParser {
-
- public static final JsonlParser DEFAULT = new JsonlParser(create());
-
- public static JsonParser.Builder create() {
- return JsonParser.create()
- .consumes("application/jsonl,application/x-ndjson,text/jsonl");
- }
-
- public JsonlParser(JsonParser.Builder builder) {
- super(builder);
- }
-
- @Override
- public JsonlParserSession.Builder createSession() {
- return JsonlParserSession.create(this);
- }
-}
-```
-
-### 4. `JsonlParserSession.java`
-
-Extends `JsonParserSession`. Overrides `doParse()` to read line-by-line and parse each line as a JSON value.
-
-```java
-package org.apache.juneau.jsonl;
-
-public class JsonlParserSession extends JsonParserSession {
-
- @Override
- protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
- try (ParserReader r = pipe.getParserReader()) {
- if (r == null)
- return null;
-
- // If target type is a collection or array, parse each line as an element
- if (type.isCollectionOrArray()) {
- ClassMeta<?> elementType = type.getElementType();
- Collection<Object> results = new JsonList(this);
- // Read lines, parse each non-empty line as JSON
- String line;
- while ((line = readLine(r)) != null) {
- if (!line.trim().isEmpty()) {
- ParserReader lineReader = createParserReader(line);
- Object item = parseAnything(elementType, lineReader, getOuter(), null);
- results.add(item);
- }
- }
- // Convert to array if needed
- return convertToType(results, type);
- }
-
- // Single object: parse just the first non-empty line
- return super.doParse(pipe, type);
- }
- }
-}
-```
-
-Key behavior:
-- Collection/array target types: reads each line, parses as JSON, collects into list, converts to target type
-- Single object target types: delegates to `JsonParserSession.doParse()` (parses the first JSON value)
-- Empty lines are skipped
-- Uses existing `parseAnything()` for all JSON parsing logic
-
-### 5. `Jsonl.java` (Marshaller)
-
-Location: `src/main/java/org/apache/juneau/marshaller/Jsonl.java`
-
-Extends `CharMarshaller`, following the exact pattern of `Json5.java`.
-
-```java
-package org.apache.juneau.marshaller;
-
-public class Jsonl extends CharMarshaller {
-
- public static final Jsonl DEFAULT = new Jsonl();
-
- public static String of(Object object) throws SerializeException {
- return DEFAULT.write(object);
- }
-
- public static Object of(Object object, Object output) throws SerializeException, IOException {
- DEFAULT.write(object, output);
- return output;
- }
-
- public static <T> T to(Object input, Class<T> type) throws ParseException, IOException {
- return DEFAULT.read(input, type);
- }
-
- public static <T> T to(Object input, Type type, Type... args) throws ParseException, IOException {
- return DEFAULT.read(input, type, args);
- }
-
- public static <T> T to(String input, Class<T> type) throws ParseException {
- return DEFAULT.read(input, type);
- }
-
- public static <T> T to(String input, Type type, Type... args) throws ParseException {
- return DEFAULT.read(input, type, args);
- }
-
- public Jsonl() {
- this(JsonlSerializer.DEFAULT, JsonlParser.DEFAULT);
- }
-
- public Jsonl(JsonlSerializer s, JsonlParser p) {
- super(s, p);
- }
-}
-```
-
-### 6. `package-info.java`
-
-Location: `src/main/java/org/apache/juneau/jsonl/package-info.java`
-
-Standard Apache license header and package-level Javadoc explaining JSONL support.
-
-### 7. `annotation/JsonlConfig.java`
-
-Annotation for specifying config properties for REST classes and methods. Empty initially (rank only), following the `MarkdownConfig`/`TomlConfig` pattern.
-
-```java
-@Target({ TYPE, METHOD })
-@Retention(RUNTIME)
-@Inherited
-@ContextApply({ JsonlConfigAnnotation.SerializerApply.class, JsonlConfigAnnotation.ParserApply.class })
-public @interface JsonlConfig {
- int rank() default 0;
-}
-```
-
-### 8. `annotation/JsonlConfigAnnotation.java`
-
-Utility class for the `@JsonlConfig` annotation, with no-op `SerializerApply` and `ParserApply` appliers (JSONL reuses `JsonSerializer.Builder`, so no format-specific settings initially).
-
----
-
-## Files to Modify
-
-### 1. REST Configuration: `BasicUniversalConfig.java`
-
-Path: `juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java`
-
-Add `JsonlSerializer.class` to the `serializers` array and `JsonlParser.class` to the `parsers` array in the `@Rest` annotation, alongside the other JSON variants.
-
-### 2. REST Configuration: `BasicJsonlConfig.java` (New)
-
-Path: `juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicJsonlConfig.java`
-
-Create a new config interface following the `BasicJson5Config` pattern:
-
-```java
-@Rest(
- serializers={JsonlSerializer.class},
- parsers={JsonlParser.class},
- defaultAccept="application/jsonl"
-)
-public interface BasicJsonlConfig extends DefaultConfig {}
-```
-
-### 3. REST Client: `RestClient.java`
-
-Path: `juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java`
-
-Add `JsonlSerializer.class` and `JsonlParser.class` to the `universal()` method's serializers/parsers lists.
-
-### 4. Context: `Context.java`
-
-Path: `juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java`
-
-Add `import org.apache.juneau.jsonl.annotation.*;` and add `{@link JsonlConfig}` to the list of config annotations in the `applyAnnotations()` Javadoc (alphabetically between JsonConfig and JsonSchemaConfig).
-
----
-
-## Test Plan
-
-All test files go in `juneau-utest/src/test/java/`.
-
-### 1. Serializer Tests: `org/apache/juneau/jsonl/Jsonl_Test.java`
-
-Test cases:
-
-- **a01_serializeCollectionOfBeans** -- Verify that a `List<Bean>` serializes to one JSON object per line with no wrapping brackets
-- **a02_serializeArray** -- Verify that a `Bean[]` array serializes identically to a collection
-- **a03_serializeSingleBean** -- Verify that a single bean serializes as a single JSON line
-- **a04_serializeCollectionOfMaps** -- Verify that `List<Map>` produces one JSON object per line
-- **a05_serializeCollectionOfStrings** -- Verify that `List<String>` produces one quoted string per line
-- **a06_serializeCollectionOfNumbers** -- Verify that `List<Integer>` produces one number per line
-- **a07_serializeEmptyCollection** -- Verify that an empty collection produces empty output
-- **a08_serializeNullValues** -- Verify null handling in collections (produces `null\n` lines)
-- **a09_serializeNestedObjects** -- Verify that beans containing nested beans/collections serialize correctly (nested structure stays on one line)
-- **a10_serializeWithSwaps** -- Verify ObjectSwap support works in JSONL context
-- **a11_noTrailingComma** -- Verify no commas between lines
-- **a12_noWhitespace** -- Verify compact output (no indentation between lines, though individual JSON values may be compact)
-
-### 2. Parser Tests: `org/apache/juneau/jsonl/JsonlParser_Test.java`
-
-Test cases:
-
-- **b01_parseToListOfBeans** -- Parse multi-line JSONL into `List<Bean>`
-- **b02_parseToArray** -- Parse multi-line JSONL into `Bean[]`
-- **b03_parseSingleLine** -- Parse single-line JSONL into a single bean
-- **b04_parseWithEmptyLines** -- Verify empty lines between JSON lines are skipped
-- **b05_parseToListOfMaps** -- Parse JSONL into `List<JsonMap>`
-- **b06_parseToListOfStrings** -- Parse JSONL with string values into `List<String>`
-- **b07_parseEmptyInput** -- Parse empty input returns empty collection or null
-- **b08_parseWithTrailingNewline** -- Verify trailing newline doesn't create extra element
-- **b09_parseNestedObjects** -- Parse JSONL with nested JSON objects
-- **b10_parseWithSwaps** -- Verify ObjectSwap support during parsing
-- **b11_parseMalformedLine** -- Verify ParseException on invalid JSON in a line
-
-### 3. Round-Trip Tests: `org/apache/juneau/jsonl/JsonlRoundTrip_Test.java`
-
-Test cases:
-
-- **c01_roundTripBeanCollection** -- Serialize `List<Bean>` to JSONL, parse back, verify equality
-- **c02_roundTripBeanArray** -- Serialize `Bean[]` to JSONL, parse back, verify equality
-- **c03_roundTripComplexBeans** -- Round-trip beans with nested objects, collections, maps
-- **c04_roundTripWithNulls** -- Round-trip collections containing null elements
-- **c05_roundTripMixedTypes** -- Round-trip `List<Object>` with mixed value types
-- **c06_roundTripWithSwaps** -- Round-trip with ObjectSwap transformations
-- **c07_roundTripSingleObject** -- Round-trip a single object (not collection)
-- **c08_roundTripEmptyCollection** -- Round-trip empty collection
-
-### 4. Marshaller Tests: `org/apache/juneau/marshaller/Jsonl_Test.java`
-
-Follow the `Json5_Test.java` pattern:
-
-- **d01_of** -- Test `Jsonl.of()` static method for serialization
-- **d02_to** -- Test `Jsonl.to()` static method for parsing
-- **d03_roundTrip** -- Test `Jsonl.of()` followed by `Jsonl.to()`
-- **d04_defaultInstance** -- Test `Jsonl.DEFAULT.write()` and `Jsonl.DEFAULT.read()`
-
-### 5. Media Type Tests: `org/apache/juneau/jsonl/JsonlMediaType_Test.java`
-
-- **e01_producesCorrectMediaType** -- Verify serializer produces `application/jsonl`
-- **e02_acceptsAllMediaTypes** -- Verify serializer accepts `application/jsonl`, `application/x-ndjson`, `text/jsonl`
-- **e03_consumesAllMediaTypes** -- Verify parser consumes all three JSONL media types
-- **e04_contentNegotiation** -- Verify correct serializer is selected via Accept header in REST context
-
-### 6. Edge Case Tests: `org/apache/juneau/jsonl/JsonlEdgeCases_Test.java`
-
-- **f01_veryLargeCollection** -- Serialize/parse a collection with 10,000+ elements
-- **f02_unicodeContent** -- JSONL with Unicode characters in values
-- **f03_specialCharactersInStrings** -- Strings containing `\n`, `\r`, `\t`, quotes
-- **f04_deeplyNestedObjects** -- Objects nested 10+ levels deep (each still on one line)
-- **f05_emptyObjects** -- Lines containing `{}` or `[]`
-- **f06_mixedLineEndings** -- Input with `\r\n` (Windows) line endings
-- **f07_whitespaceOnlyLines** -- Lines with only spaces/tabs are treated as empty
-- **f08_booleanAndNullLines** -- Lines containing bare `true`, `false`, `null`
-- **f09_numericLines** -- Lines containing bare numbers
-
----
-
-### 7. Cross-Format Round-Trip Integration: `org/apache/juneau/a/rttests/RoundTripDateTime_Test.java`
-
-Add a `RoundTrip_Tester` entry for this format to the `TESTERS` array in `RoundTripDateTime_Test.java`. This test verifies date/time and Duration round-trip across all serializer/parser combinations. It covers beans with `Instant`, `ZonedDateTime`, `LocalDate`, `LocalDateTime`, `LocalTime`, `OffsetDateTime`, `OffsetTime`, `Year`, `YearMonth`, `Calendar`, `Date`, `Duration`, and `XMLGregorianCalendar` fields.
-
-Insert the entry **after the existing JSON testers (after the entry with ID 3)** and **before the XML testers (currently ID 4)**. All subsequent tester IDs shift up by 1.
-
-```java
-tester(4, "Jsonl - default")
- .serializer(JsonlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
- .parser(JsonlParser.create())
- .skipIf(x -> !(x instanceof Collection || x.getClass().isArray()))
- .build(),
-```
-
-The `.skipIf()` guard is required because the round-trip test passes individual typed objects (not collections) to the tester. JSONL only guarantees lossless round-trip for collections and arrays at the top level; parsing JSONL to a single non-collection type returns only the first line, which does not provide a complete round-trip for the multi-field date/time beans used by this test. The guard skips all non-collection/non-array test objects for the JSONL tester.
-
-## Documentation
-
-### 1. Javadoc
-
-Each new class must have comprehensive Javadoc following the existing Juneau patterns, using Juneau's custom HTML tags (`<jc>`, `<jsf>`, `<jsm>`, `<jv>`, `<bc>`, `<bjson>`, `<bjava>`, `<c>`) for code formatting.
-
----
-
-#### `JsonlSerializer` — Class-level Javadoc template
-
-Follows the `YamlSerializer` structure exactly:
-
-```
-/**
- * Serializes POJO models to JSONL (JSON Lines).
- *
- * <h5 class='topic'>Media types</h5>
- * <p>
- * Handles <c>Accept</c> types: <bc>application/jsonl, application/x-ndjson, text/jsonl</bc>
- * <p>
- * Produces <c>Content-Type</c> types: <bc>application/jsonl</bc>
- *
- * <h5 class='topic'>Description</h5>
- * <p>
- * Serializes POJOs to JSONL format where each top-level value is written as a compact JSON
- * value on its own line. The conversion is as follows...
- * <ul class='spaced-list'>
- * <li>Maps and beans are converted to JSON objects, one per line.
- * <li>Collections and arrays are converted to one JSON value per line (no wrapping brackets).
- * <li>Strings are converted to JSON strings, one per line.
- * <li>Numbers are converted to JSON numbers, one per line.
- * <li>Booleans are converted to JSON booleans, one per line.
- * <li>Nulls are converted to JSON nulls, one per line.
- * <li>Nested objects and collections remain compact on a single line per outer element.
- * </ul>
- * <p>
- * Non-JSON-primitive types are transformed through
- * {@link org.apache.juneau.swap.ObjectSwap ObjectSwaps}.
- *
- * <h5 class='section'>Example:</h5>
- * <p class='bjava'>
- * <jc>// Use the default serializer to serialize a list of POJOs</jc>
- * String <jv>jsonl</jv> = JsonlSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>myList</jv>);
- *
- * <jc>// Create a custom serializer</jc>
- * JsonlSerializer <jv>serializer</jv> = JsonlSerializer.<jsm>create</jsm>().build();
- *
- * <jc>// Clone an existing serializer</jc>
- * <jv>serializer</jv> = JsonlSerializer.<jsf>DEFAULT</jsf>.copy().build();
- *
- * <jc>// Serialize a list of POJOs to JSONL</jc>
- * String <jv>jsonl</jv> = <jv>serializer</jv>.serialize(<jv>myList</jv>);
- * </p>
- *
- * <h5 class='figure'>Example output (List of beans):</h5>
- * <p class='bjson'>
- * {"name":"Alice","age":30}
- * {"name":"Bob","age":25}
- * {"name":"Carol","age":35}
- * </p>
- *
- * <h5 class='figure'>Complex (bean with nested object and array):</h5>
- * <p class='bjson'>
- * {"name":"Alice","age":30,"address":{"street":"123 Main St","city":"Boston"},"tags":["a","b","c"]}
- * {"name":"Bob","age":25,"address":{"street":"456 Oak Ave","city":"Portland"},"tags":["d","e"]}
- * </p>
- *
- * <h5 class='section'>Limitations compared to JSON</h5>
- * <p>
- * <ul class='spaced-list'>
- * <li><b>No pretty-printing</b>: {@link org.apache.juneau.serializer.WriterSerializer.Builder#useWhitespace() useWhitespace()}
- * is always suppressed. JSONL requires each JSON value to fit on a single line;
- * applying intra-line pretty-printing would embed newlines and produce invalid JSONL.
- * The setting is silently ignored regardless of its configured value.
- * <li><b>Top-level semantics differ</b>: Collections and arrays are unwrapped — each element
- * becomes one line. A single non-collection object produces one JSONL line.
- * </ul>
- *
- * <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/JsonlBasics">JSONL Basics</a>
- * </ul>
- */
-```
-
----
-
-#### `JsonlParser` — Class-level Javadoc template
-
-Follows the `JsonParser` structure:
-
-```
-/**
- * Parses JSONL (JSON Lines) input into POJO models.
- *
- * <h5 class='topic'>Media types</h5>
- * <p>
- * Handles <c>Content-Type</c> types: <bc>application/jsonl, application/x-ndjson, text/jsonl</bc>
- *
- * <h5 class='topic'>Description</h5>
- * <p>
- * Parses JSONL input where each non-empty line is a complete JSON value. Blank lines are ignored.
- * The conversion is as follows...
- * <ul class='spaced-list'>
- * <li>JSON objects (<c>{...}</c>) are converted to {@link org.apache.juneau.collections.JsonMap JsonMaps}
- * or Java beans if a target type is provided.
- * <li>JSON arrays (<c>[...]</c>) are converted to {@link org.apache.juneau.collections.JsonList JsonLists}.
- * <li>JSON strings are converted to {@link java.lang.String Strings}.
- * <li>JSON numbers are converted to {@link java.lang.Integer Integers}, {@link java.lang.Long Longs},
- * {@link java.lang.Float Floats}, or {@link java.lang.Double Doubles}.
- * <li>JSON booleans are converted to {@link java.lang.Boolean Booleans}.
- * <li>JSON nulls are converted to <jk>null</jk>.
- * </ul>
- * <p>
- * When the target type is a {@link java.util.Collection Collection} or array, each line is parsed
- * as one element. When the target type is a single value, only the first non-empty line is parsed.
- *
- * <h5 class='section'>Example:</h5>
- * <p class='bjava'>
- * <jc>// Use the default parser to parse JSONL into a list of POJOs</jc>
- * List<MyBean> <jv>list</jv> = JsonlParser.<jsf>DEFAULT</jsf>.parse(<jv>jsonlInput</jv>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
- *
- * <jc>// Create a custom parser</jc>
- * JsonlParser <jv>parser</jv> = JsonlParser.<jsm>create</jsm>().build();
- *
- * <jc>// Clone an existing parser</jc>
- * <jv>parser</jv> = JsonlParser.<jsf>DEFAULT</jsf>.copy().build();
- * </p>
- *
- * <h5 class='figure'>Example input:</h5>
- * <p class='bjson'>
- * {"name":"Alice","age":30}
- * {"name":"Bob","age":25}
- * {"name":"Carol","age":35}
- * </p>
- *
- * <h5 class='section'>Limitations compared to JSON</h5>
- * <p>
- * <ul class='spaced-list'>
- * <li><b>No multi-line JSON input</b>: Each JSON value must be on a single line.
- * Multi-line (pretty-printed) JSON piped as JSONL input will cause parse errors.
- * <li><b>First-line-only for non-collection targets</b>: When parsing JSONL to a single
- * non-collection type, only the first non-empty line is parsed; trailing lines are
- * silently ignored.
- * </ul>
- *
- * <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/JsonlBasics">JSONL Basics</a>
- * </ul>
- */
-```
-
----
-
-#### `Jsonl` (Marshaller) — Class-level Javadoc template
-
-Follows the `Json5.java` marshaller structure:
-
-```
-/**
- * Pairs {@link JsonlSerializer} and {@link JsonlParser} into a single class with convenience
- * read/write methods.
- *
- * <h5 class='figure'>Examples:</h5>
- * <p class='bjava'>
- * <jc>// Serialize to JSONL using instance</jc>
- * Jsonl <jv>jsonl</jv> = <jk>new</jk> Jsonl();
- * String <jv>out</jv> = <jv>jsonl</jv>.write(<jv>myList</jv>);
- * List<MyBean> <jv>in</jv> = <jv>jsonl</jv>.read(<jv>out</jv>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
- *
- * <jc>// Serialize to JSONL using DEFAULT instance</jc>
- * String <jv>out</jv> = Jsonl.<jsm>of</jsm>(<jv>myList</jv>);
- * List<MyBean> <jv>in</jv> = Jsonl.<jsm>to</jsm>(<jv>out</jv>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
- * </p>
- *
- * <h5 class='figure'>Example output (List of beans):</h5>
- * <p class='bjson'>
- * {"name":"Alice","age":30}
- * {"name":"Bob","age":25}
- * {"name":"Carol","age":35}
- * </p>
- *
- * <h5 class='figure'>Complex (bean with nested object and array):</h5>
- * <p class='bjson'>
- * {"name":"Alice","age":30,"address":{"street":"123 Main St","city":"Boston"},"tags":["a","b","c"]}
- * {"name":"Bob","age":25,"address":{"street":"456 Oak Ave","city":"Portland"},"tags":["d","e"]}
- * </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>
- */
-```
-
-`of()` and `to()` static method javadoc follows the rigid `Json5.java` template:
-- One-sentence summary naming the format and direction (e.g., "Serializes a Java object to a JSONL string.")
-- `<p>A shortcut for calling <c><jsf>DEFAULT</jsf>.write(<jv>object</jv>)</c>.</p>`
-- `@param` for each argument — with a `<ul>` listing all accepted subtypes for polymorphic `Object input` / `Object output` params
-- `@return` on its own indented line
-- `@throws SerializeException` / `@throws ParseException` / `@throws IOException` as applicable
-- `@see BeanSession#getClassMeta` only on the `Type...args` overload
-
----
-
-### 2. Package Javadoc (`package-info.java`)
-
-Explain:
-- What JSONL is and when to use it
-- Relationship to JSON serializer/parser
-- AI/ML use cases (fine-tuning datasets, streaming inference, log aggregation)
-- Code examples showing basic usage
-
-### 3. Update Release Notes
-/Users/james.bognar/git/apache/juneau/docs/pages/release-notes
-
-### 4. Update Documentation
-
-Find how existing languages are documented in the following location and update the documentation to match
-the same level of detail.
-/Users/james.bognar/git/apache/juneau/docs/pages/topics
-
----
-
-## Implementation Order
-
-1. `JsonlSerializer.java` and `JsonlSerializerSession.java` -- Core serialization
-2. `JsonlParser.java` and `JsonlParserSession.java` -- Core parsing
-3. `Jsonl.java` (marshaller) -- Convenience API
-4. `package-info.java` -- Package documentation
-5. Serializer tests (`Jsonl_Test.java`)
-6. Parser tests (`JsonlParser_Test.java`)
-7. Round-trip tests (`JsonlRoundTrip_Test.java`)
-8. Marshaller tests (marshaller `Jsonl_Test.java`)
-9. Media type tests (`JsonlMediaType_Test.java`)
-10. Edge case tests (`JsonlEdgeCases_Test.java`)
-11. REST integration (`BasicUniversalConfig`, `BasicJsonlConfig`, `RestClient`)
-12. Context registration (`JsonlConfig`, `JsonlConfigAnnotation`, `Context.java`)
-13. Final documentation review and Javadoc polish
-
----
-
-## Iterator/Iterable/Stream Support
-
-As of 9.2.1, Juneau natively supports serialization of `Iterator`, `Iterable` (non-Collection), `Enumeration`, and `java.util.stream.Stream` types.
-
-JSONL extends `JsonSerializer` but overrides `doSerialize()` to write one object per line. The top-level collection is consumed in `doSerialize()`, so streamable handling must be added there rather than relying on the base class `serializeAnything()`.
-
-In `doSerialize()`, add a branch for streamable types alongside the existing `isCollection()/isArray()` checks:
-
-```java
-} else if (cm != null && cm.isStreamable()) {
- ClassMeta<?> elementType = cm.getElementType();
- forEachStreamableEntry(o, cm, item -> {
- serializeAnything(w.i(getInitialDepth()), item, elementType, "root", null);
- w.w('\n');
- });
-}
-```
-
-This uses `forEachStreamableEntry()` from `SerializerSession` for lazy iteration, writing each element as a separate JSON line without materializing the entire collection into memory first.
-
----
-
-## Key Design Decisions
-
-1. **Extend, don't duplicate**: JSONL classes extend JSON classes rather than copying code. All JSON serialization/parsing logic is inherited; only the collection-level behavior changes.
-
-2. **No separate Builder**: Following `Json5Serializer` precedent, `JsonlSerializer` reuses `JsonSerializer.Builder`. No JSONL-specific configuration properties are needed.
-
-3. **Package location**: New `org.apache.juneau.jsonl` package (not inside `org.apache.juneau.json`) because JSONL has its own media type and semantics distinct from JSON. This follows the pattern where each format has its own package.
-
-4. **Whitespace handling**: `useWhitespace()` is **always suppressed** in `JsonlSerializerSession`. JSONL requires each JSON value to fit on a single line; applying intra-line pretty-printing would embed newlines and produce invalid JSONL. The setting is silently ignored regardless of its configured value. In `doSerialize()`, the writer is always used in compact mode. This is the primary behavioral difference from `JsonSerializer`.
-
-5. **Single object fallback**: When serializing/parsing a non-collection object, JSONL behaves identically to JSON (single line of JSON). This makes JSONL a superset of JSON for single-object use cases.
-
-6. **Line reading strategy for parser**: The parser reads input line by line rather than using the character-level JSON parser state machine for line splitting. This is correct per the JSONL spec which mandates `\n` as the line separator.
-
-7. **Date/time and Duration support**: Date/time types (Date, Calendar, java.time Temporal subtypes like Instant, ZonedDateTime, LocalDate, etc.) and java.time.Duration are inherited from the JSON serializer dispatch since `JsonlSerializerSession` delegates to `JsonSerializerSession.serializeAnything()`. They are serialized as ISO 8601 strings, one JSON value per line. Parsing uses `Iso8601Utils.parse()` to convert ISO 8601 strings back to the target types.
diff --git a/todo/protobuf_implementation.md b/todo/protobuf_implementation.md
deleted file mode 100644
index c4c5120a49..0000000000
--- a/todo/protobuf_implementation.md
+++ /dev/null
@@ -1,1124 +0,0 @@
-# Protocol Buffers Text Format Support Implementation Plan for Apache Juneau
-
-## Overview
-
-This plan covers implementing serialization and parsing support for the Protocol Buffers Text Format in Juneau. This is the **text format** (human-readable, used for protobuf configurations and debugging), NOT the binary wire format. The text format is schema-less in Juneau's usage -- Java bean metadata provides the type information that `.proto` files would normally supply.
-
-The protobuf text format is widely recognized in the gRPC ecosystem and provides a clean, human-readable representation of structured data that is syntactically distinct from JSON (no commas between fields, no quotes on field names, optional colons before messages).
-
-**Lossless round-trip marshalling** is a primary goal. Every data structure supported by the protobuf text format serializer must produce output that, when parsed back, yields an object equal to the original. This is equivalent to the JSON round-trip guarantee. See the "JSON Data Structure Parity and Limitations" section for the narrow set of cases where full JSON parity is not achievable.
-
-## Specification
-
-- **Text Format Spec**: https://protobuf.dev/reference/protobuf/textformat-spec
-- **Media type**: `text/protobuf`, `text/x-protobuf`
-- **Encoding**: UTF-8
-
-## Protobuf Text Format Syntax
-
-```
-name: "John"
-age: 42
-active: true
-address {
- city: "Springfield"
- state: "IL"
-}
-hobbies: ["reading", "gaming", "hiking"]
-```
-
-Key syntax rules:
-- **Field names**: Bare identifiers (no quotes)
-- **Scalar fields**: `field_name: value` (colon required)
-- **Message fields**: `field_name { ... }` or `field_name: { ... }` (colon optional)
-- **Strings**: Double-quoted `"..."` or single-quoted `'...'` with C-style escaping
-- **Numbers**: Integers (decimal, hex `0x`, octal `0`), floats
-- **Booleans**: `true`/`false`
-- **No null**: Absent fields represent unset values
-- **Collections**: List syntax `[val1, val2, val3]` or repeated field names
-- **Comments**: `# comment to end of line`
-- **Field separators**: Newlines, semicolons, or commas (all optional between fields)
-- **No trailing commas required**: Unlike JSON
-
----
-
-## Bean-to-Protobuf Text Format Mapping
-
-### Mapping Rules
-
-**1. Simple bean properties -> scalar fields:**
-
-```java
-class Server { String host; int port; boolean debug; double ratio; }
-```
-```
-host: "localhost"
-port: 8080
-debug: true
-ratio: 3.14
-```
-
-**2. Nested bean properties -> message fields:**
-
-```java
-class Config { String name; Database db; }
-class Database { String host; int port; }
-```
-```
-name: "myapp"
-db {
- host: "localhost"
- port: 5432
-}
-```
-
-No colon before the opening brace for message fields (following protobuf convention). The serializer omits it for message values and includes it for scalar values.
-
-**3. Deeply nested beans -> nested message fields:**
-
-```java
-class Config { Server server; }
-class Server { String host; Ssl ssl; }
-class Ssl { boolean enabled; String cert; }
-```
-```
-server {
- host: "localhost"
- ssl {
- enabled: true
- cert: "/path/to/cert.pem"
- }
-}
-```
-
-**4. Collections of simple values -> list syntax:**
-
-```java
-class Config { List<String> tags; List<Integer> ports; }
-```
-```
-tags: ["web", "api", "rest"]
-ports: [8080, 8443, 9090]
-```
-
-**5. Collections of beans -> repeated message fields:**
-
-```java
-class Config { List<Server> servers; }
-class Server { String host; int port; }
-```
-```
-servers {
- host: "alpha"
- port: 8080
-}
-servers {
- host: "beta"
- port: 8081
-}
-```
-
-Repeated message fields follow protobuf convention: the field name is repeated for each element (not list syntax, which is for scalars only).
-
-**6. Maps -> message fields:**
-
-```java
-class Config { Map<String,String> env; }
-```
-```
-env {
- PATH: "/usr/bin"
- HOME: "/home/user"
-}
-```
-
-Map keys that are valid identifiers use bare syntax. Keys requiring quoting use quoted key syntax.
-
-**7. Null handling:**
-
-Protobuf text format has no null representation. Null properties are omitted. During parsing, missing fields leave bean properties at their Java defaults.
-
-**8. Enum values:**
-
-```java
-class Config { LogLevel level; }
-enum LogLevel { DEBUG, INFO, WARN, ERROR }
-```
-```
-level: INFO
-```
-
-Enum values are serialized as unquoted identifiers (matching protobuf convention).
-
-**9. String escaping:**
-
-Protobuf text format uses C-style escape sequences: `\"`, `\\`, `\n`, `\t`, `\r`, `\b`, `\f`, `\'`, `\a`, `\v`, `\xHH` (hex byte), `\OOO` (octal byte), `\uHHHH` (Unicode), `\UHHHHHHHH` (extended Unicode).
-
-**10. Type mappings:**
-
-| Java Type | Protobuf Text Format | Example |
-|-----------|---------------------|---------|
-| `String` | Quoted string | `name: "John"` |
-| `int`, `long` | Decimal integer | `count: 42` |
-| `float`, `double` | Float literal | `ratio: 3.14` |
-| `boolean` | `true`/`false` | `active: true` |
-| `Enum` | Identifier (unquoted) | `level: INFO` |
-| `null` | (omitted) | Field absent |
-| Nested bean | Message `{ ... }` | `addr { city: "NYC" }` |
-| `Map<String,V>` | Message `{ ... }` | `env { KEY: "val" }` |
-| `List<simple>` | List `[v1, v2]` | `tags: ["a", "b"]` |
-| `List<bean>` | Repeated fields | Multiple `name { ... }` |
-| `byte[]` | Quoted string (escaped bytes) | `data: "\x0a\x05"` |
-
----
-
-## JSON Data Structure Parity and Limitations
-
-The protobuf text format supports the same data structures as JSON with the following mapping:
-
-### Fully Supported (Lossless Round-Trip)
-
-| JSON structure | Protobuf text equivalent | Lossless? |
-|---|---|---|
-| `String` | Quoted string | Yes |
-| `int`/`long`/`float`/`double` | Numeric literal | Yes (with typed target) |
-| `boolean` | `true`/`false` | Yes |
-| Beans/Maps (objects) | Message fields `{ }` | Yes |
-| `List<String>` / `List<Number>` | List syntax `[v1, v2]` | Yes |
-| `List<Bean>` | Repeated field names | Yes |
-| `byte[]` | C-style escaped bytes in string | Yes |
-| `Enum` | Unquoted identifier | Yes |
-| `Date`/`Calendar`/`Temporal` subtypes | ISO 8601 string | Yes (via swap) |
-| `Duration` | ISO 8601 duration string | Yes (via swap) |
-| `Optional<T>` | Same as T or omitted | Yes |
-
-### Limitations Compared to JSON
-
-- **No native null representation** — Null properties are omitted during serialization. On parsing, missing fields revert to Java defaults. For reference types (`String`, boxed types), the Java default is `null`, so this is lossless. For primitives (`int`, `boolean`), null is not possible in Java, so there is no loss.
-- **No top-level scalars** — Protobuf text format requires a message (object) at the top level. Serializing a bare `String`, `int`, or `boolean` as the root object wraps it in a synthetic single-field message. This is a limitation vs. JSON, which supports any value at the root.
-- **Untyped parsing loses numeric precision** — When parsing into `JsonMap`/`JsonList` (untyped), integer vs. long vs. double is inferred from the literal. With typed targets (bean classes), this is fully lossless.
-- **Map keys must be strings** — JSON supports any string key including those with special characters. Protobuf text format uses bare identifiers for valid keys and quoted strings for others. Lossless as long as quoted key syntax is used for non-identifier keys.
-- **Angle-bracket messages** (`< >`) are accepted on parse but are never emitted on serialize.
-
----
-
-## Architecture
-
-The protobuf text format is a text-based format with unique syntax (not JSON), so it extends `WriterSerializer`/`ReaderParser` directly and has its own tokenizer.
-
-```
-WriterSerializer
- └── ProtoSerializer
- └── ProtoSerializerSession (uses ProtoWriter)
-
-ReaderParser
- └── ProtoParser
- └── ProtoParserSession (uses ProtoTokenizer)
-```
-
----
-
-## Files to Create
-
-All source files in `juneau-core/juneau-marshall/src/main/java/org/apache/juneau/proto/`.
-
-### 1. `ProtoSerializer.java`
-
-Extends `WriterSerializer`. Implements `ProtoMetaProvider`.
-
-Class-level javadoc (matching `JsonSerializer` style):
-
-```java
-/**
- * Serializes POJO models to Protobuf Text Format.
- *
- * <h5 class='topic'>Media types</h5>
- * <p>
- * Handles <c>Accept</c> types: <bc>text/protobuf, text/x-protobuf</bc>
- * <p>
- * Produces <c>Content-Type</c> types: <bc>text/protobuf</bc>
- *
- * <h5 class='topic'>Description</h5>
- * <p>
- * Implements the <a class="doclink" href="https://protobuf.dev/reference/protobuf/textformat-spec">Protobuf Text Format</a>
- * serializer. This is the human-readable text format used for protobuf configuration files and debugging,
- * NOT the binary wire format.
- * <p>
- * The conversion is as follows...
- * <ul class='spaced-list'>
- * <li>
- * Maps (e.g. {@link HashMap}, {@link TreeMap}) and beans are converted to protobuf message fields.
- * <li>
- * Collections (e.g. {@link HashSet}, {@link LinkedList}) and Java arrays of simple values are converted to
- * protobuf list syntax {@code [v1, v2, ...]}.
- * <li>
- * Collections of beans are converted to repeated field names.
- * <li>
- * {@link String Strings} are converted to quoted strings with C-style escaping.
- * <li>
- * {@link Number Numbers} (e.g. {@link Integer}, {@link Long}, {@link Double}) are converted to numeric literals.
- * <li>
- * {@link Boolean Booleans} are converted to {@code true}/{@code false}.
- * <li>
- * Enum values are converted to unquoted identifiers.
- * <li>
- * {@code null} properties are omitted (protobuf text format has no null representation).
- * <li>
- * {@code byte[]} is converted to a quoted string using C-style hex escaping.
- * </ul>
- *
- * <p>
- * Date/time types ({@code Calendar}, {@code Date}, {@code Temporal} subtypes, {@code Duration}) are serialized as
- * ISO 8601 strings. Swaps can be used to convert non-serializable POJOs into serializable forms or to override
- * the default format for types that already have built-in support.
- *
- * <h5 class='topic'>Behavior-specific subclasses</h5>
- * <p>
- * The following direct subclasses are provided for convenience:
- * <ul class='spaced-list'>
- * <li>
- * {@link ProtoSerializer.Readable} - Default serializer with additional indentation spacing.
- * </ul>
- *
- * <h5 class='section'>Example:</h5>
- * <p class='bjava'>
- * <jc>// Use one of the default serializers to serialize a POJO</jc>
- * String <jv>proto</jv> = ProtoSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>someObject</jv>);
- *
- * <jc>// Create a custom serializer</jc>
- * ProtoSerializer <jv>serializer</jv> = ProtoSerializer.<jsm>create</jsm>().addBeanTypes().build();
- *
- * <jc>// Clone an existing serializer and modify it</jc>
- * <jv>serializer</jv> = ProtoSerializer.<jsf>DEFAULT</jsf>.copy().useColonForMessages().build();
- *
- * <jc>// Serialize a POJO to Protobuf Text Format</jc>
- * String <jv>proto</jv> = <jv>serializer</jv>.serialize(<jv>someObject</jv>);
- * </p>
- *
- * <h5 class='figure'>Example output (Map of name/age):</h5>
- * <p class='bcode'>
- * name: "Alice"
- * age: 30
- * </p>
- *
- * <h5 class='figure'>Complex (nested object + array):</h5>
- * <p class='bcode'>
- * name: "Alice"
- * age: 30
- * address {
- * street: "123 Main St"
- * city: "Boston"
- * state: "MA"
- * }
- * tags: ["a", "b", "c"]
- * </p>
- *
- * <h5 class='section'>See Also:</h5><ul>
- * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ProtobufBasics">Protobuf Text Format Basics</a>
- * <li class='link'><a class="doclink" href="https://protobuf.dev/reference/protobuf/textformat-spec">Protobuf Text Format Specification</a>
- * </ul>
- */
-```
-
-Class structure:
-
-```java
-package org.apache.juneau.proto;
-
-public class ProtoSerializer extends WriterSerializer implements ProtoMetaProvider {
-
- public static final ProtoSerializer DEFAULT = ...;
- public static final ProtoSerializer DEFAULT_READABLE = ...; // with extra spacing
-
- public static class Builder extends WriterSerializer.Builder {
- boolean useListSyntaxForBeans = false; // Use [...] for bean collections too
- boolean useColonForMessages = false; // Use ":" before { for message fields
-
- protected Builder() {
- produces("text/protobuf");
- accept("text/protobuf,text/x-protobuf");
- }
- }
-
- @Override
- public ProtoSerializerSession.Builder createSession() {
- return ProtoSerializerSession.create(this);
- }
-}
-```
-
-Builder options:
-- `useListSyntaxForBeans(boolean)` -- Use `[{...}, {...}]` list syntax for bean collections instead of repeated field names (non-standard but more compact; default: false)
-- `useColonForMessages(boolean)` -- Include colon before message values (`field: { ... }` vs `field { ... }`; default: false, matching protobuf convention)
-
-### 2. `ProtoSerializerSession.java`
-
-Extends `WriterSerializerSession`. Core serialization logic.
-
-Key methods:
-- `doSerialize(SerializerPipe, Object)` -- Entry point
-- `serializeAnything(ProtoWriter, Object, ClassMeta, String, BeanPropertyMeta)` -- Central dispatch
-- `serializeBeanMap(ProtoWriter, BeanMap, String)` -- Serializes bean properties as fields
-- `serializeMap(ProtoWriter, Map, ClassMeta)` -- Serializes map entries as fields
-- `serializeCollection(ProtoWriter, Collection, ClassMeta, String)` -- Renders as list syntax or repeated fields
-- `serializeScalarValue(ProtoWriter, Object, ClassMeta)` -- Writes typed scalar value
-
-Serialization algorithm:
-```
-serializeBeanMap(writer, beanMap, typeName):
- for each (key, value) in beanMap:
- if value is null: continue (omit)
-
- if value is simple (string, number, boolean, enum, date/calendar/temporal, duration):
- writer.fieldName(key)
- writer.w(": ")
- serializeScalarValue(writer, value, classMeta)
- writer.newLine()
-
- else if value is bean:
- writer.fieldName(key)
- writer.w(" {")
- writer.newLine()
- indent++
- serializeBeanMap(writer, toBeanMap(value))
- indent--
- writer.indent().w("}")
- writer.newLine()
-
- else if value is map:
- writer.fieldName(key)
- writer.w(" {")
- writer.newLine()
- indent++
- serializeMap(writer, value, classMeta)
- indent--
- writer.indent().w("}")
- writer.newLine()
-
- else if value is collection of simple values:
- writer.fieldName(key)
- writer.w(": [")
- // comma-separated values
- writer.w("]")
- writer.newLine()
-
- else if value is collection of beans:
- for each item in collection:
- writer.fieldName(key)
- writer.w(" {")
- writer.newLine()
- indent++
- serializeBeanMap(writer, toBeanMap(item))
- indent--
- writer.indent().w("}")
- writer.newLine()
-```
-
-### 3. `ProtoWriter.java`
-
-Extends `SerializerWriter`. Provides protobuf text format-specific write methods.
-
-```java
-package org.apache.juneau.proto;
-
-public class ProtoWriter extends SerializerWriter {
-
- // Field writing
- ProtoWriter fieldName(String name); // Writes bare identifier or quoted key
- ProtoWriter scalarField(String name, Object value); // name: value\n
- ProtoWriter messageStart(String name); // name {\n
- ProtoWriter messageEnd(); // }\n
-
- // Value writing
- ProtoWriter stringValue(String value); // "value" with C-style escaping
- ProtoWriter integerValue(long value); // 42
- ProtoWriter floatValue(double value); // 3.14 (handles inf, nan)
- ProtoWriter booleanValue(boolean value); // true/false
- ProtoWriter enumValue(String name); // IDENTIFIER (unquoted)
- ProtoWriter bytesValue(byte[] data); // "\x0a\x05..."
-
- // List writing
- ProtoWriter listStart(); // [
- ProtoWriter listEnd(); // ]
-
- // Formatting
- ProtoWriter comment(String text); // # text
- ProtoWriter indent(); // Write current indentation
-
- // Key encoding
- boolean isBareIdentifier(String name); // Can be written without quotes
- String escapeString(String text); // C-style escape sequences
-}
-```
-
-Key encoding rules:
-- Bare identifiers: `A-Za-z_` followed by `A-Za-z0-9_` (protobuf IDENT rule)
-- Non-identifier keys (from maps with special-character keys) are quoted: `"my.key": value`
-
-### 4. `ProtoParser.java`
-
-Extends `ReaderParser`. Implements `ProtoMetaProvider`.
-
-Class-level javadoc (matching `JsonParser` style):
-
-```java
-/**
- * Parses Protobuf Text Format into a POJO model.
- *
- * <h5 class='topic'>Media types</h5>
- * <p>
- * Handles <c>Content-Type</c> types: <bc>text/protobuf, text/x-protobuf</bc>
- *
- * <h5 class='topic'>Description</h5>
- * <p>
- * This parser uses a state machine with a tokenizer, making it efficient for parsing protobuf
- * text format without requiring an intermediate DOM representation.
- *
- * <p>
- * Handles all valid Protobuf Text Format syntax including:
- * <ul class='spaced-list'>
- * <li>
- * Message fields using brace delimiters ({@code field { ... }}).
- * <li>
- * Scalar fields using colon syntax ({@code field: value}).
- * <li>
- * List syntax ({@code field: [v1, v2]}) for collections of simple values.
- * <li>
- * Repeated field names aggregated into collections of beans.
- * <li>
- * Integers: decimal, hex ({@code 0x...}), octal ({@code 0...}).
- * <li>
- * Floats including special values {@code inf}, {@code -inf}, and {@code nan}.
- * <li>
- * Single and double quoted strings with C-style escape sequences ({@code \n}, {@code \t},
- * {@code \\}, {@code \"}, {@code \xHH}, {@code \OOO}, {@code \uHHHH}).
- * <li>
- * Multi-part adjacent string concatenation (e.g. {@code "hello" "world"}).
- * <li>
- * Angle-bracket message delimiters ({@code < >}) as an alternative to braces.
- * <li>
- * Comments ({@code # ...} to end of line).
- * <li>
- * Optional field separators: newlines, semicolons, or commas.
- * </ul>
- *
- * <p>
- * This parser handles the following input and automatically returns the corresponding Java class:
- * <ul class='spaced-list'>
- * <li>
- * Protobuf messages are converted to {@link JsonMap JsonMaps}.
- * <b>Note:</b> If a <code><xa>_type</xa>=<xs>'xxx'</xs></code> field is present, then an
- * attempt is made to convert the message to an instance of the specified Java bean class.
- * <li>
- * List fields are converted to {@link JsonList JsonLists}.
- * <li>
- * Quoted strings are converted to {@link String Strings}.
- * <li>
- * Numeric literals are converted to {@link Integer Integers}, {@link Long Longs},
- * or {@link Double Doubles} depending on size and presence of a decimal point.
- * <li>
- * Booleans ({@code true}/{@code false}) are converted to {@link Boolean Booleans}.
- * <li>
- * Unquoted identifiers are converted to {@link String Strings} or enum constants
- * depending on the target property type.
- * </ul>
- *
- * <h5 class='section'>Example:</h5>
- * <p class='bjava'>
- * <jc>// Use the default parser to parse into a bean</jc>
- * MyBean <jv>bean</jv> = ProtoParser.<jsf>DEFAULT</jsf>.parse(<jv>input</jv>, MyBean.<jk>class</jk>);
- *
- * <jc>// Parse into an untyped map</jc>
- * JsonMap <jv>map</jv> = ProtoParser.<jsf>DEFAULT</jsf>.parse(<jv>input</jv>, JsonMap.<jk>class</jk>);
- *
- * <jc>// Create a strict-mode parser</jc>
- * ProtoParser <jv>parser</jv> = ProtoParser.<jsm>create</jsm>().strict().build();
- * MyBean <jv>bean</jv> = <jv>parser</jv>.parse(<jv>input</jv>, MyBean.<jk>class</jk>);
- * </p>
- *
- * <h5 class='section'>See Also:</h5><ul>
- * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ProtobufBasics">Protobuf Text Format Basics</a>
- * <li class='link'><a class="doclink" href="https://protobuf.dev/reference/protobuf/textformat-spec">Protobuf Text Format Specification</a>
- * </ul>
- */
-```
-
-Class structure:
-
-```java
-package org.apache.juneau.proto;
-
-public class ProtoParser extends ReaderParser implements ProtoMetaProvider {
-
- public static final ProtoParser DEFAULT = ...;
-
- public static class Builder extends ReaderParser.Builder {
- protected Builder() {
- consumes("text/protobuf,text/x-protobuf");
- }
- }
-
- @Override
- public ProtoParserSession.Builder createSession() {
- return ProtoParserSession.create(this);
- }
-}
-```
-
-### 5. `ProtoParserSession.java`
-
-Extends `ReaderParserSession`. Core parsing logic.
-
-Parsing strategy -- two-phase (same as TOML/INI):
-1. Tokenize and build intermediate `Map<String, Object>` structure
-2. Populate target bean from intermediate structure
-
-Key methods:
-- `doParse(ParserPipe, ClassMeta)` -- Entry point
-- `parseMessage(ProtoTokenizer)` -- Parses a message (between `{` and `}` or at top level) into a `Map<String, Object>`
-- `parseValue(ProtoTokenizer, ClassMeta)` -- Parses a scalar or message value
-- `parseList(ProtoTokenizer)` -- Parses `[val1, val2, ...]`
-- `populateBean(Map, BeanMap)` -- Maps intermediate structure to bean properties
-
-Parsing algorithm:
-```
-parseMessage(tokenizer):
- result = new LinkedHashMap()
- while not EOF and not "}":
- fieldName = readFieldName()
-
- if next token is ":":
- consume ":"
- if next token is "[":
- value = parseList()
- else:
- value = parseScalarValue()
- else if next token is "{":
- value = parseMessage() // nested message
-
- // Handle repeated fields (same name appears multiple times)
- if result contains fieldName:
- existing = result.get(fieldName)
- if existing is not List:
- existing = new ArrayList(existing)
- result.put(fieldName, existing)
- existing.add(value)
- else:
- result.put(fieldName, value)
-
- // Consume optional separator (;, comma, or newline)
- return result
-```
-
-Scalar values (strings, numbers, booleans, identifiers) are parsed by `parseScalarValue()`. Date/time strings in ISO 8601 format are converted to target types (Date, Calendar, Temporal subtypes) via `Iso8601Utils.parse()` and `BeanSession.convertToMemberType()`. Duration strings are similarly converted for Duration-typed properties.
-
-### 6. `ProtoTokenizer.java`
-
-Internal tokenizer for protobuf text format.
-
-```java
-package org.apache.juneau.proto;
-
-class ProtoTokenizer {
- ProtoTokenizer(Reader reader);
-
- Token peek(); // Look ahead without consuming
- Token read(); // Read and consume next token
- void skipWhitespaceAndComments();
-
- // Token types
- enum TokenType {
- IDENT, // bare_identifier
- STRING, // "quoted" or 'quoted'
- DEC_INT, // 42, -17
- OCT_INT, // 0755
- HEX_INT, // 0xDEAD
- FLOAT, // 3.14, 1e06, 10f
- LBRACE, // {
- RBRACE, // }
- LANGLE, // <
- RANGLE, // >
- LBRACKET, // [
- RBRACKET, // ]
- COLON, // :
- COMMA, // ,
- SEMICOLON, // ;
- EOF
- }
-
- // Value reading
- String readIdentifier(); // Bare identifier
- String readString(); // Quoted string with unescaping
- Number readInteger(); // Decimal, hex, or octal integer
- double readFloat(); // Float including inf, nan
- boolean readBoolean(); // true/false
-
- int getLine(); // For error messages
- int getColumn();
-}
-```
-
-### 7. `ProtoMetaProvider.java`
-
-```java
-package org.apache.juneau.proto;
-
-public interface ProtoMetaProvider {
- ProtoClassMeta getProtoClassMeta(ClassMeta<?> cm);
- ProtoBeanPropertyMeta getProtoBeanPropertyMeta(BeanPropertyMeta bpm);
-}
-```
-
-### 8. `ProtoClassMeta.java` and `ProtoBeanPropertyMeta.java`
-
-Extend `ExtendedClassMeta` and `ExtendedBeanPropertyMeta`.
-
-`ProtoBeanPropertyMeta` fields:
-- `comment` (String) -- Comment to emit before this field
-
-### 9. `annotation/Proto.java`
-
-```java
-@Target({TYPE, METHOD, FIELD})
-@Retention(RUNTIME)
-public @interface Proto {
- String comment() default ""; // Comment before field
- String on() default "";
- String[] onClass() default {};
-}
-```
-
-### 10. `annotation/ProtoConfig.java`
-
-```java
-@Target({TYPE, METHOD})
-@Retention(RUNTIME)
-public @interface ProtoConfig {
- String useListSyntaxForBeans() default "";
- String useColonForMessages() default "";
- int rank() default 0;
-}
-```
-
-### 11. `annotation/ProtoAnnotation.java` and `annotation/ProtoConfigAnnotation.java`
-
-Utility classes for programmatic annotation creation.
-
-### 12. `Proto.java` (Marshaller)
-
-Location: `src/main/java/org/apache/juneau/marshaller/Proto.java`
-
-Extends `CharMarshaller`.
-
-Class-level javadoc (matching `Json.java` style):
-
-```java
-/**
- * A pairing of a {@link ProtoSerializer} and {@link ProtoParser} into a single class with convenience read/write methods.
- *
- * <p>
- * The general idea is to combine a single serializer and parser inside a simplified API for reading and writing POJOs.
- *
- * <h5 class='figure'>Examples:</h5>
- * <p class='bjava'>
- * <jc>// Using instance.</jc>
- * Proto <jv>proto</jv> = <jk>new</jk> Proto();
- * MyPojo <jv>myPojo</jv> = <jv>proto</jv>.read(<jv>string</jv>, MyPojo.<jk>class</jk>);
- * String <jv>string</jv> = <jv>proto</jv>.write(<jv>myPojo</jv>);
- * </p>
- * <p class='bjava'>
- * <jc>// Using DEFAULT instance.</jc>
- * MyPojo <jv>myPojo</jv> = Proto.<jsf>DEFAULT</jsf>.read(<jv>string</jv>, MyPojo.<jk>class</jk>);
- * String <jv>string</jv> = Proto.<jsf>DEFAULT</jsf>.write(<jv>myPojo</jv>);
- * </p>
- *
- * <h5 class='figure'>Example output (Map of name/age):</h5>
- * <p class='bcode'>
- * name: "Alice"
- * age: 30
- * </p>
- *
- * <h5 class='figure'>Complex (nested object + array):</h5>
- * <p class='bcode'>
- * name: "Alice"
- * age: 30
- * address {
- * street: "123 Main St"
- * city: "Boston"
- * state: "MA"
- * }
- * tags: ["a", "b", "c"]
- * </p>
- *
- * <h5 class='section'>See Also:</h5><ul>
- * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Marshallers">Marshallers</a>
- * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ProtobufBasics">Protobuf Text Format Basics</a>
- * </ul>
- */
-```
-
-Class structure:
-
-```java
-package org.apache.juneau.marshaller;
-
-public class Proto extends CharMarshaller {
- public static final Proto DEFAULT = new Proto();
-
- public static String of(Object object) throws SerializeException { ... }
- public static Object of(Object object, Object output) throws SerializeException, IOException { ... }
- public static <T> T to(Object input, Class<T> type) throws ParseException, IOException { ... }
- public static <T> T to(Object input, Type type, Type...args) throws ParseException, IOException { ... }
- public static <T> T to(String input, Class<T> type) throws ParseException { ... }
- public static <T> T to(String input, Type type, Type...args) throws ParseException { ... }
-
- public Proto() { this(ProtoSerializer.DEFAULT, ProtoParser.DEFAULT); }
- public Proto(ProtoSerializer s, ProtoParser p) { super(s, p); }
-}
-```
-
-Javadoc for `of(Object)` static method (matching `Json.of` style):
-
-```java
-/**
- * Serializes a Java object to a Protobuf Text Format 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.
- */
-```
-
-Javadoc for `to(String, Class)` static method:
-
-```java
-/**
- * Parses a Protobuf Text Format 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.
- */
-```
-
-### 13. `package-info.java`
-
-Package-level Javadoc.
-
----
-
-## Files to Modify
-
-### 1. `BasicUniversalConfig.java`
-
-Add `ProtoSerializer.class` to `serializers` and `ProtoParser.class` to `parsers`.
-
-### 2. `BasicProtoConfig.java` (New)
-
-```java
-@Rest(
- serializers={ProtoSerializer.class},
- parsers={ProtoParser.class},
- defaultAccept="text/protobuf"
-)
-public interface BasicProtoConfig extends DefaultConfig {}
-```
-
-### 3. `RestClient.java`
-
-Add `ProtoSerializer.class` and `ProtoParser.class` to the `universal()` method.
-
-### 4. Context: `Context.java`
-
-Path: `juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java`
-
-Add `import org.apache.juneau.proto.annotation.*;` and add `{@link ProtoConfig}` to the list of config annotations in the `applyAnnotations()` Javadoc (alphabetically).
-
----
-
-## Test Plan
-
-All test files in `juneau-utest/src/test/java/`.
-
-### 1. Serializer Tests: `org/apache/juneau/proto/ProtoSerializer_Test.java`
-
-- **a01_simpleBean** -- String, int, boolean, float properties serialize as scalar fields
-- **a02_nestedBean** -- Nested bean serializes as message field with braces
-- **a03_deeplyNestedBean** -- 3+ levels of nesting
-- **a04_collectionOfStrings** -- `List<String>` serializes as list syntax `["a", "b"]`
-- **a05_collectionOfIntegers** -- `List<Integer>` serializes as list syntax `[1, 2, 3]`
-- **a06_collectionOfBeans** -- `List<Bean>` serializes as repeated message fields
-- **a07_mapProperty** -- `Map<String,String>` serializes as message field
-- **a08_nullValues** -- Null properties omitted
-- **a09_booleanValues** -- `true`/`false` lowercase
-- **a10_floatValues** -- Proper float formatting (handles `inf`, `nan`)
-- **a11_stringEscaping** -- C-style escaping (`\n`, `\t`, `\\`, `\"`)
-- **a12_enumValues** -- Enums as unquoted identifiers
-- **a13_emptyBean** -- Empty bean produces empty output
-- **a14_emptyCollections** -- Empty list produces `field_name: []`
-- **a15_objectSwaps** -- Swap transformations before encoding
-- **a16_noColonBeforeMessages** -- Message fields use `name { ... }` not `name: { ... }`
-- **a17_indentation** -- Proper indentation at each nesting level
-- **a18_typeName** -- `_type` property when `addBeanTypes` enabled
-- **a19_specialMapKeys** -- Map keys with dots/spaces use quoted key syntax
-- **a20_binaryData** -- `byte[]` as hex-escaped string
-
-### 2. Parser Tests: `org/apache/juneau/proto/ProtoParser_Test.java`
-
-- **b01_parseSimpleBean** -- Scalar fields to bean properties
-- **b02_parseNestedBean** -- Message field `{ ... }` to nested bean
-- **b03_parseDeeplyNestedBean** -- Multi-level nesting
-- **b04_parseListOfStrings** -- `["a", "b"]` to `List<String>`
-- **b05_parseListOfIntegers** -- `[1, 2, 3]` to `List<Integer>`
-- **b06_parseRepeatedMessages** -- Repeated field names to `List<Bean>`
-- **b07_parseMapProperty** -- Message field to `Map<String,String>`
-- **b08_parseBooleans** -- `true`/`false` to boolean
-- **b09_parseIntegers** -- Decimal, hex (`0x`), octal (`0`) integers
-- **b10_parseFloats** -- Float literals, `inf`, `-inf`, `nan`
-- **b11_parseStrings** -- Double-quoted and single-quoted with escaping
-- **b12_parseMultiPartStrings** -- Adjacent strings concatenated (`"hello" "world"`)
-- **b13_parseComments** -- `#` comments ignored
-- **b14_parseSemicolonSeparators** -- Fields separated by `;`
-- **b15_parseCommaSeparators** -- Fields separated by `,`
-- **b16_parseMissingFields** -- Missing fields leave defaults
-- **b17_parseAngleBrackets** -- `< ... >` as alternative message delimiters
-- **b18_parseColonBeforeMessage** -- `field: { ... }` accepted (colon optional)
-- **b19_parseEnumValues** -- Unquoted identifiers to enum
-- **b20_parseQuotedKeys** -- `"special.key": value` parsed correctly
-
-### 3. Round-Trip Tests: `org/apache/juneau/proto/ProtoRoundTrip_Test.java`
-
-- **c01_simpleBeanRoundTrip** -- All simple types round-trip
-- **c02_nestedBeanRoundTrip** -- Nested beans round-trip
-- **c03_collectionOfBeansRoundTrip** -- Repeated fields round-trip to bean collections
-- **c04_collectionOfStringsRoundTrip** -- String lists round-trip
-- **c05_mapRoundTrip** -- Map properties round-trip
-- **c06_enumRoundTrip** -- Enum values round-trip
-- **c07_stringEscapingRoundTrip** -- Strings with special chars round-trip
-- **c08_booleanRoundTrip** -- Boolean values round-trip
-- **c09_numericEdgeCases** -- Integer boundaries, float precision
-- **c10_complexBeanRoundTrip** -- Bean with mix of all types
-- **c11_objectSwapRoundTrip** -- Swapped values round-trip
-- **c12_emptyCollectionsRoundTrip** -- Empty lists round-trip as `[]`
-- **c13_nullableReferenceRoundTrip** -- Null `String`, `Integer`, and `Long` fields are omitted on serialize and return `null` on parse, confirming lossless behavior for reference types
-- **c14_topLevelScalarLimitation** -- Documents that bare scalars (e.g. a plain `String` or `int`) at the root level must be wrapped in a synthetic single-field message to round-trip; asserts the wrapping and unwrapping behavior
-- **c15_untypedJsonMapRoundTrip** -- A `JsonMap` with a mix of string, integer, boolean, nested map, and list values round-trips through `ProtoSerializer`/`ProtoParser` with untyped parsing
-
-### 4. Tokenizer Tests: `org/apache/juneau/proto/ProtoTokenizer_Test.java`
-
-- **d01_bareIdentifier** -- `field_name`, `_private`, `camelCase`
-- **d02_quotedString** -- `"hello"`, `'hello'` with escape sequences
-- **d03_multiPartString** -- `"part1" "part2"` concatenation
-- **d04_decimalInteger** -- `42`, `+42`, `-17`
-- **d05_hexInteger** -- `0xDEAD`, `0xFF`
-- **d06_octalInteger** -- `0755`, `0123`
-- **d07_floatLiteral** -- `3.14`, `1e06`, `10f`, `.5`
-- **d08_specialFloats** -- `inf`, `-inf`, `nan`, `infinity`
-- **d09_booleans** -- `true`, `false` (also `True`, `t`, `1`, `0`)
-- **d10_comments** -- `# comment to end of line`
-- **d11_structuralTokens** -- `{`, `}`, `<`, `>`, `[`, `]`, `:`, `,`, `;`
-- **d12_stringEscapes** -- `\n`, `\t`, `\\`, `\"`, `\'`, `\xHH`, `\OOO`, `\uHHHH`
-- **d13_edgeCaseNumberIdent** -- `10bar` is invalid (number followed by ident)
-- **d14_whitespaceHandling** -- Tabs, newlines, carriage returns
-
-### 5. Marshaller Tests: `org/apache/juneau/marshaller/Proto_Test.java`
-
-- **e01_of** -- `Proto.of(bean)` serializes to string
-- **e02_to** -- `Proto.to(input, Type)` parses from string
-- **e03_roundTrip** -- `Proto.of()` + `Proto.to()` round-trip
-- **e04_defaultInstance** -- `Proto.DEFAULT.write()` and `Proto.DEFAULT.read()`
-
-### 6. Annotation Tests: `org/apache/juneau/proto/ProtoAnnotation_Test.java`
-
-- **f01_comment** -- `@Proto(comment="...")` emits comment before field
-- **f02_annotationEquivalency** -- Annotation equivalency checks
-
-### 7. Edge Case Tests: `org/apache/juneau/proto/ProtoEdgeCases_Test.java`
-
-- **g01_emptyInput** -- Empty input returns null/empty bean
-- **g02_onlyComments** -- Document with only comments returns null/empty bean
-- **g03_unicodeStrings** -- Unicode in keys and values
-- **g04_veryLongStrings** -- Very long string values
-- **g05_deeplyNestedMessages** -- 10+ levels of nesting
-- **g06_windowsLineEndings** -- `\r\n` handled correctly
-- **g07_mixedSeparators** -- Mix of newlines, semicolons, and commas
-- **g08_cyclicReferences** -- Handled by recursion detection
-- **g09_optionalProperties** -- `Optional<T>` bean properties
-- **g10_mapWithNonStringKeys** -- Integer/enum map keys encoded as strings
-- **g11_emptyMessages** -- `field {}` parsed as empty bean
-- **g12_trailingComma** -- Trailing comma after last field accepted
-
-### 8. Media Type Tests: `org/apache/juneau/proto/ProtoMediaType_Test.java`
-
-- **h01_producesCorrectMediaType** -- Serializer produces `text/protobuf`
-- **h02_consumesCorrectMediaType** -- Parser consumes `text/protobuf`, `text/x-protobuf`
-- **h03_contentNegotiation** -- Correct serializer selected via Accept header
-
----
-
-### 9. Cross-Format Round-Trip Integration: `org/apache/juneau/a/rttests/RoundTripDateTime_Test.java`
-
-Add a `RoundTrip_Tester` entry for this format to the `TESTERS` array in `RoundTripDateTime_Test.java`. This test verifies date/time and Duration round-trip across all serializer/parser combinations. It covers beans with `Instant`, `ZonedDateTime`, `LocalDate`, `LocalDateTime`, `LocalTime`, `OffsetDateTime`, `OffsetTime`, `Year`, `YearMonth`, `Calendar`, `Date`, `Duration`, and `XMLGregorianCalendar` fields.
-
-## Documentation
-
-### 1. Javadoc
-
-**`ProtoSerializer.java`:**
-- Description of protobuf text format (link to spec)
-- Clarification: text format, not binary wire format
-- Media types: `text/protobuf`
-- Bean-to-protobuf mapping rules with examples
-- Comparison with JSON (similar data model, different syntax)
-- Use cases: gRPC ecosystem integration, configuration files, debugging
-- Thread safety notes
-
-**`ProtoParser.java`:**
-- Supported syntax elements
-- Type inference rules (how unquoted values map to Java types)
-- Repeated field handling (aggregated into collections)
-- Error reporting (line/column numbers)
-
-**`ProtoWriter.java`:**
-- Each method documented with protobuf text format output examples
-- Escaping rules
-
-**`ProtoTokenizer.java`:**
-- Internal class (package-private)
-- Token types and lexical rules
-
-### 2. Package Javadoc (`package-info.java`)
-
-- What protobuf text format is and how it differs from binary protobuf
-- No `.proto` schema files needed (Java bean metadata serves as schema)
-- Complete mapping rules table
-- Usage examples for serialization and parsing
-- gRPC ecosystem context
-- Comparison with JSON, TOML, INI
-
-### 3. Update Release Notes
-/Users/james.bognar/git/apache/juneau/docs/pages/release-notes
-
-### 4. Update Documentation
-
-Find how existing languages are documented in the following location and update the documentation to match
-the same level of detail.
-/Users/james.bognar/git/apache/juneau/docs/pages/topics
-
----
-
-## Implementation Order
-
-1. **`ProtoTokenizer.java`** -- Foundation for parsing
-2. **`ProtoWriter.java`** -- Foundation for serialization
-3. **`ProtoSerializer.java`** and **`ProtoSerializerSession.java`** -- Core serialization
-4. **`ProtoParser.java`** and **`ProtoParserSession.java`** -- Core parsing
-5. **`ProtoMetaProvider.java`**, **`ProtoClassMeta.java`**, **`ProtoBeanPropertyMeta.java`** -- Metadata
-6. **`annotation/Proto.java`**, **`annotation/ProtoConfig.java`** -- Annotations
-7. **`annotation/ProtoAnnotation.java`**, **`annotation/ProtoConfigAnnotation.java`** -- Annotation utilities
-8. **`Proto.java`** (marshaller) -- Convenience API
-9. **`package-info.java`** -- Package documentation
-10. **Tokenizer tests** (`ProtoTokenizer_Test.java`) -- 14 test cases
-11. **Serializer tests** (`ProtoSerializer_Test.java`) -- 20 test cases
-12. **Parser tests** (`ProtoParser_Test.java`) -- 20 test cases
-13. **Round-trip tests** (`ProtoRoundTrip_Test.java`) -- 12 test cases
-14. **Marshaller tests** (`Proto_Test.java`)
-15. **Annotation tests** (`ProtoAnnotation_Test.java`)
-16. **Edge case tests** (`ProtoEdgeCases_Test.java`) -- 12 test cases
-17. **Media type tests** (`ProtoMediaType_Test.java`)
-18. **REST integration** (`BasicUniversalConfig`, `BasicProtoConfig`, `RestClient`)
-19. **Context registration** (`ProtoConfig`, `ProtoConfigAnnotation`, `Context.java`)
-20. **Final documentation review**
-
----
-
-## Key Design Decisions
-
-### 1. Text format, not binary wire format
-
-Implementing protobuf binary wire format without `.proto` schema files is impractical -- the binary format uses field numbers (not names) and wire types that require schema knowledge to encode/decode. The text format uses field names and is self-describing, making it ideal for Juneau's schema-less bean serialization. The text format is widely used in protobuf configurations, tests, and debugging output.
-
-### 2. Extends WriterSerializer/ReaderParser
-
-Since this is a text format (not binary), it extends `WriterSerializer`/`ReaderParser` like JSON, XML, and other text serializers. The output is UTF-8 text.
-
-### 3. No external dependencies
-
-The protobuf text format grammar is simpler than TOML. The tokenizer handles: identifiers, quoted strings with C-style escaping, integers (decimal/hex/octal), floats (including `inf`/`nan`), comments, and structural tokens (`{}`, `<>`, `[]`, `:`, `,`, `;`).
-
-### 4. Repeated field names for bean collections
-
-Following protobuf convention, collections of beans use repeated field names rather than list syntax:
-```
-servers { host: "a" }
-servers { host: "b" }
-```
-Not: `servers: [{host: "a"}, {host: "b"}]`
-
-This is the standard protobuf text format convention. The parser aggregates fields with the same name into collections. The `useListSyntaxForBeans` builder option enables the non-standard but more compact list syntax if desired.
-
-### 5. Optional colon before message values
-
-Following protobuf convention, the colon before message values is omitted by default:
-```
-address {
- city: "NYC"
-}
-```
-The `useColonForMessages` builder option enables colons for consistency with scalar fields if preferred.
-
-### 6. Null omission
-
-Protobuf has no null type. Null properties are omitted during serialization. During parsing, missing fields retain their Java defaults. This is consistent with protobuf semantics where unset fields have default values.
-
-### 7. Type inference during parsing
-
-The parser uses the target bean's `ClassMeta` for type resolution (same strategy as INI and TOML parsers). Unquoted identifiers are parsed as booleans (`true`/`false`) or enum values depending on the target property type. Numbers are parsed as integers or floats based on the target type.
-
-### 8. Package name: `org.apache.juneau.proto`
-
-Short form `proto` rather than `protobuf` to keep the package name concise, consistent with protobuf ecosystem conventions where `proto` is the standard abbreviation.
-
-### 9. Lossless round-trip at JSON parity
-
-The serializer/parser pair must pass the same round-trip tests as JSON for all supported POJO types. Where the format imposes a structural limitation (top-level scalars, null omission), it is documented explicitly in the "JSON Data Structure Parity and Limitations" section and tested to confirm the documented behavior. The `ProtoRoundTrip_Test.java` test class and the `RoundTripDateTime_Test.java` cross-format integration tests enforce this guarantee.
-
----
-
-## Iterator/Iterable/Stream Support
-
-As of 9.2.1, Juneau natively supports serialization of `Iterator`, `Iterable` (non-Collection), `Enumeration`, and `java.util.stream.Stream` types.
-
-This format uses the **lazy (streaming) path**: elements are written directly to the output one at a time without materializing into an intermediate List.
-
-In `serializeAnything()`, add a branch after the `isCollection()/isArray()` checks:
-
-```java
-} else if (sType.isStreamable()) {
- serializeStreamable(out, o, sType, eType);
-}
-```
-
-Add a `serializeStreamable()` method using `forEachStreamableEntry()` from SerializerSession, following the same output pattern as `serializeCollection()` but without requiring a Collection parameter.
-
----
-
-## Scope Exclusions
-
-The following are explicitly not included in this implementation:
-
-- **Binary wire format** -- Requires `.proto` schema files with field numbers and wire types
-- **`.proto` file generation** -- Generating `.proto` schemas from Java beans (possible future feature)
-- **Extension fields** -- `[com.example.ext]` syntax (requires type registry)
-- **Any fields** -- `[type.googleapis.com/...]` syntax (requires type registry)
-- **Group fields** -- Legacy proto2 feature
-- **Half-precision floats** -- Not part of protobuf text format
-- **Interoperability with protobuf binary messages** -- This is a text-format-only serializer