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/02/28 16:26:07 UTC

(juneau) branch master updated: YAML 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 c0fa710662 YAML support
c0fa710662 is described below

commit c0fa710662a6f936e65415713994436ec05a8605
Author: James Bognar <ja...@salesforce.com>
AuthorDate: Sat Feb 28 11:26:03 2026 -0500

    YAML support
---
 RELEASE-NOTES.txt                                  |   32 +-
 TODO.md                                            |    8 +-
 .../src/main/java/org/apache/juneau/MediaType.java |    2 +
 .../java/org/apache/juneau/marshaller/Yaml.java    |  227 +++
 .../java/org/apache/juneau/yaml/YamlParser.java    |  634 +++++++++
 .../org/apache/juneau/yaml/YamlParserSession.java  | 1456 ++++++++++++++++++++
 .../org/apache/juneau/yaml/YamlSerializer.java     |  907 ++++++++++++
 .../apache/juneau/yaml/YamlSerializerSession.java  |  453 ++++++
 .../java/org/apache/juneau/yaml/YamlWriter.java    |  409 ++++++
 .../apache/juneau/yaml/annotation/YamlConfig.java  |   78 ++
 .../yaml/annotation/YamlConfigAnnotation.java      |   72 +
 .../juneau/yaml/annotation/package-info.java       |   20 +
 .../java/org/apache/juneau/yaml/package-info.java  |   20 +
 .../juneau/rest/config/BasicUniversalConfig.java   |    7 +-
 .../apache/juneau/rest/config/BasicYamlConfig.java |   74 +
 .../org/apache/juneau/ComboRoundTripTest_Base.java |   66 +
 .../org/apache/juneau/ComboRoundTrip_Tester.java   |   12 +-
 .../juneau/a/rttests/RoundTripDateTime_Test.java   |    5 +
 .../juneau/a/rttests/RoundTripTest_Base.java       |    5 +
 .../apache/juneau/yaml/YamlMarshaller_Test.java    |   73 +
 .../org/apache/juneau/yaml/YamlParser_Test.java    |  144 ++
 .../java/org/apache/juneau/yaml/Yaml_Test.java     |  119 ++
 22 files changed, 4801 insertions(+), 22 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index c231a807f3..d8060c3256 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -26,7 +26,6 @@ Release Notes - Juneau - Version 9.2.1 - YYYY-MM-DD
     * Bump Bump org.apache.maven.plugins:maven-compiler-plugin from 3.14.1 to 3.15.0 #314.
     * Bump Bump jetty.version from 12.1.5 to 12.1.6 #312.
 
-<<<<<<< Updated upstream
 ** New Features - Direct Iterator/Iterable/Stream/Enumeration Serialization
 
     * Iterators, Iterables (non-Collection), Enumerations, and java.util.stream.Streams
@@ -37,21 +36,30 @@ Release Notes - Juneau - Version 9.2.1 - YYYY-MM-DD
       since the format requires knowing the size or inspecting elements upfront.
     * New ClassMeta methods: isIterator(), isIterable(), isStream(), isStreamable().
     * New ClassMeta.Category enum values: ITERATOR, ITERABLE, STREAM.
-=======
-** New Features - Direct Iterator/Iterable/Stream Serialization
-
-    * Iterators, Iterables (non-Collection), Enumerations, and java.util.stream.Streams
-      are now serialized directly as arrays without requiring intermediate List materialization.
-    * For text-based serializers (JSON, XML, UON), elements are written lazily one at a
-      time for improved memory efficiency with large datasets.
-    * For binary/structured serializers (MsgPack, HTML, CSV), elements are collected to a
-      List internally since the format requires knowing the size or inspecting elements upfront.
-    * New ClassMeta methods: isIterator(), isIterable(), isStream(), isStreamable().
->>>>>>> Stashed changes
     * New SerializerSession methods: forEachStreamableEntry(), toListFromStreamable().
     * IteratorSwap and EnumerationSwap classes have been removed; Iterator, Enumeration, Iterable,
       and Stream types are now handled natively by all serializers.
 
+** New Features - Built-in Date/Time and Duration Serialization
+
+    * Date, time, and duration types are now serialized and parsed natively as ISO 8601
+      strings by all serializers and parsers without requiring ObjectSwap registration.
+    * Supported types:
+        - All java.time.temporal.Temporal subtypes (Instant, ZonedDateTime, OffsetDateTime,
+          LocalDateTime, LocalDate, LocalTime, OffsetTime, Year, YearMonth)
+        - java.util.Calendar and java.util.Date
+        - javax.xml.datatype.XMLGregorianCalendar
+        - java.time.Duration (serialized as ISO 8601 duration strings, e.g. "PT1H30M")
+    * New ClassMeta.Category enum value: DURATION.
+    * New ClassMeta method: isDuration().
+    * New utility class: Iso8601Utils - centralizes ISO 8601 formatting and parsing.
+    * Default date/time swaps removed from DefaultSwaps; existing swap classes
+      (TemporalCalendarSwap, TemporalDateSwap, CalendarSwap, DateSwap, TemporalSwap)
+      remain available as override mechanisms for customizing output format.
+    * BeanSession.convertToMemberType() and SerializerSession.toString() updated to
+      use ISO 8601 formatting for date/time/Duration types.
+    * All parser sessions updated with date/time/Duration dispatch in parseAnything().
+
 Release Notes - Juneau - Version 9.2.0 - 2025-12-30
 
 ** Changes
diff --git a/TODO.md b/TODO.md
index 4981320347..a3557e6056 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,18 +1,12 @@
 # TODO
 
-- Find all places where we have system properties defined for juneau and convert them to use the new Settings class.
 - Document "juneau.enableVerboseExceptions" setting.
 - Change field names that start with underscore so that they end with underscore (e.g. "_enum"->"enum_")
 - Find places where we defined fields and methods as _foobar and convert them to foobar_
 - Investigate navlinks URL generation issue: Either "request:?Accept=text/json&plainText=true" should be supported, or "request:/?Accept=text/json&plainText=true" should not append '/' to the request URL. Currently, "request:/?Accept=..." generates URLs like "http://localhost:5000/rest/db/request:?Accept=..." which is incorrect.
-- Determine if it's possible to add a "short" field to @Schema for AI purposes.
-- JsonSchemaParser should be able to produce JsonSchema beans.
-- JsonSchemaGenerator should return JsonSchema beans.
-- Create full-fledged CSV serializer/parser support.
-- Add YAML serializer/parser support.
 - Update REST server API to use new BeanStore2.
 - ClassInfo should have a findGetter(String propertyName) convenience method.
 - Make sure @Beanp("*") works on plain fields.
-- Add schema validation to beans during parsing.
+
 
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MediaType.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MediaType.java
index 076b7709e6..b311b7d836 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MediaType.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MediaType.java
@@ -66,6 +66,8 @@ public class MediaType implements Comparable<MediaType> {
 	public static final MediaType URLENCODING = of("application/x-www-form-urlencoded");
 	/** Reusable predefined media type for XML */
 	public static final MediaType XML = of("text/xml");
+	/** Reusable predefined media type for YAML */
+	public static final MediaType YAML = of("application/yaml");
 	/** Reusable predefined media type for SOAP XML */
 	public static final MediaType XMLSOAP = of("text/xml+soap");
 	/** Reusable predefined media type for RDF XML */
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Yaml.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Yaml.java
new file mode 100644
index 0000000000..4df7a24ebe
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Yaml.java
@@ -0,0 +1,227 @@
+/*
+ * 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 java.nio.charset.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.yaml.*;
+
+/**
+ * A pairing of a {@link YamlSerializer} and {@link YamlParser} 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>
+ * 	Yaml <jv>yaml</jv> = <jk>new</jk> Yaml();
+ * 	MyPojo <jv>myPojo</jv> = <jv>yaml</jv>.read(<jv>string</jv>, MyPojo.<jk>class</jk>);
+ * 	String <jv>string</jv> = <jv>yaml</jv>.write(<jv>myPojo</jv>);
+ * </p>
+ * <p class='bjava'>
+ *	<jc>// Using DEFAULT instance.</jc>
+ * 	MyPojo <jv>myPojo</jv> = Yaml.<jsf>DEFAULT</jsf>.read(<jv>string</jv>, MyPojo.<jk>class</jk>);
+ * 	String <jv>string</jv> = Yaml.<jsf>DEFAULT</jsf>.write(<jv>myPojo</jv>);
+ * </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 Yaml extends CharMarshaller {
+
+	/**
+	 * Default reusable instance.
+	 */
+	public static final Yaml DEFAULT = new Yaml();
+
+	/**
+	 * Serializes a Java object to a YAML 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 YAML output.
+	 *
+	 * <p>
+	 * A shortcut for calling <c><jsf>DEFAULT</jsf>.write(<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 YAML input object to the specified Java type.
+	 *
+	 * <p>
+	 * A shortcut for calling <c><jsf>DEFAULT</jsf>.read(<jv>input</jv>, <jv>type</jv>)</c>.
+	 *
+	 * @param <T> The class type of the object being created.
+	 * @param input
+	 * 	The input.
+	 * 	<br>Can be any of the following types:
+	 * 	<ul>
+	 * 		<li><jk>null</jk>
+	 * 		<li>{@link Reader}
+	 * 		<li>{@link CharSequence}
+	 * 		<li>{@link InputStream} containing UTF-8 encoded text (or charset defined by
+	 * 			{@link org.apache.juneau.parser.ReaderParser.Builder#streamCharset(Charset)} property value).
+	 * 		<li><code><jk>byte</jk>[]</code> containing UTF-8 encoded text (or charset defined by
+	 * 			{@link org.apache.juneau.parser.ReaderParser.Builder#streamCharset(Charset)} property value).
+	 * 		<li>{@link File} containing system encoded text (or charset defined by
+	 * 			{@link org.apache.juneau.parser.ReaderParser.Builder#fileCharset(Charset)} property value).
+	 * 	</ul>
+	 * @param type The object type to create.
+	 * @return The parsed object.
+	 * @throws ParseException Malformed input encountered.
+	 * @throws IOException Thrown by underlying stream.
+	 */
+	public static <T> T to(Object input, Class<T> type) throws ParseException, IOException {
+		return DEFAULT.read(input, type);
+	}
+
+	/**
+	 * Parses a YAML input object to the specified Java type.
+	 *
+	 * <p>
+	 * A shortcut for calling <c><jsf>DEFAULT</jsf>.read(<jv>input</jv>, <jv>type</jv>, <jv>args</jv>)</c>.
+	 *
+	 * @param <T> The class type of the object to create.
+	 * @param input
+	 * 	The input.
+	 * 	<br>Can be any of the following types:
+	 * 	<ul>
+	 * 		<li><jk>null</jk>
+	 * 		<li>{@link Reader}
+	 * 		<li>{@link CharSequence}
+	 * 		<li>{@link InputStream} containing UTF-8 encoded text (or charset defined by
+	 * 			{@link org.apache.juneau.parser.ReaderParser.Builder#streamCharset(Charset)} property value).
+	 * 		<li><code><jk>byte</jk>[]</code> containing UTF-8 encoded text (or charset defined by
+	 * 			{@link org.apache.juneau.parser.ReaderParser.Builder#streamCharset(Charset)} property value).
+	 * 		<li>{@link File} containing system encoded text (or charset defined by
+	 * 			{@link org.apache.juneau.parser.ReaderParser.Builder#fileCharset(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 YAML 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 YAML 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 YamlSerializer#DEFAULT} and {@link YamlParser#DEFAULT}.
+	 */
+	public Yaml() {
+		this(YamlSerializer.DEFAULT, YamlParser.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 Yaml(YamlSerializer s, YamlParser p) {
+		super(s, p);
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
new file mode 100644
index 0000000000..626d2552d8
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
@@ -0,0 +1,634 @@
+/*
+ * 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.yaml;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.lang.annotation.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.commons.function.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Parses YAML text into a POJO model.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Handles <c>Content-Type</c> types:  <bc>application/yaml, text/yaml</bc>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * 	<li class='note'>This class is thread safe and reusable.
+ * </ul>
+ */
+@SuppressWarnings({
+	"java:S110", // Inheritance depth acceptable
+	"java:S115" // Constants use UPPER_snakeCase convention
+})
+public class YamlParser extends ReaderParser {
+
+	private static final String ARG_copyFrom = "copyFrom";
+
+	/**
+	 * Builder class.
+	 */
+	public static class Builder extends ReaderParser.Builder {
+
+		private static final Cache<HashKey,YamlParser> CACHE = Cache.of(HashKey.class, YamlParser.class).build();
+
+		/**
+		 * Constructor, default settings.
+		 */
+		protected Builder() {
+			consumes("application/yaml,text/yaml");
+		}
+
+		/**
+		 * Copy constructor.
+		 *
+		 * @param copyFrom The builder to copy from.
+		 * 	<br>Cannot be <jk>null</jk>.
+		 */
+		protected Builder(Builder copyFrom) {
+			super(assertArgNotNull(ARG_copyFrom, copyFrom));
+		}
+
+		/**
+		 * Copy constructor.
+		 *
+		 * @param copyFrom The bean to copy from.
+		 * 	<br>Cannot be <jk>null</jk>.
+		 */
+		protected Builder(YamlParser copyFrom) {
+			super(assertArgNotNull(ARG_copyFrom, copyFrom));
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder annotations(Annotation...values) {
+			super.annotations(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder apply(AnnotationWorkList work) {
+			super.apply(work);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder applyAnnotations(Class<?>...from) {
+			super.applyAnnotations(from);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder applyAnnotations(Object...from) {
+			super.applyAnnotations(from);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder autoCloseStreams() {
+			super.autoCloseStreams();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder autoCloseStreams(boolean value) {
+			super.autoCloseStreams(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanClassVisibility(Visibility value) {
+			super.beanClassVisibility(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanConstructorVisibility(Visibility value) {
+			super.beanConstructorVisibility(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanContext(BeanContext value) {
+			super.beanContext(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanContext(BeanContext.Builder value) {
+			super.beanContext(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanDictionary(java.lang.Class<?>...values) {
+			super.beanDictionary(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanFieldVisibility(Visibility value) {
+			super.beanFieldVisibility(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanInterceptor(Class<?> on, Class<? extends org.apache.juneau.swap.BeanInterceptor<?>> value) {
+			super.beanInterceptor(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanMapPutReturnsOldValue() {
+			super.beanMapPutReturnsOldValue();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanMethodVisibility(Visibility value) {
+			super.beanMethodVisibility(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanProperties(Class<?> beanClass, String properties) {
+			super.beanProperties(beanClass, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanProperties(Map<String,Object> values) {
+			super.beanProperties(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanProperties(String beanClassName, String properties) {
+			super.beanProperties(beanClassName, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesExcludes(Class<?> beanClass, String properties) {
+			super.beanPropertiesExcludes(beanClass, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesExcludes(Map<String,Object> values) {
+			super.beanPropertiesExcludes(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesExcludes(String beanClassName, String properties) {
+			super.beanPropertiesExcludes(beanClassName, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesReadOnly(Class<?> beanClass, String properties) {
+			super.beanPropertiesReadOnly(beanClass, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesReadOnly(Map<String,Object> values) {
+			super.beanPropertiesReadOnly(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesReadOnly(String beanClassName, String properties) {
+			super.beanPropertiesReadOnly(beanClassName, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesWriteOnly(Class<?> beanClass, String properties) {
+			super.beanPropertiesWriteOnly(beanClass, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesWriteOnly(Map<String,Object> values) {
+			super.beanPropertiesWriteOnly(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesWriteOnly(String beanClassName, String properties) {
+			super.beanPropertiesWriteOnly(beanClassName, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beansRequireDefaultConstructor() {
+			super.beansRequireDefaultConstructor();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beansRequireSerializable() {
+			super.beansRequireSerializable();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beansRequireSettersForGetters() {
+			super.beansRequireSettersForGetters();
+			return this;
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public YamlParser build() {
+			return cache(CACHE).build(YamlParser.class);
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) {
+			super.cache(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder consumes(String value) {
+			super.consumes(value);
+			return this;
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public Builder copy() {
+			return new Builder(this);
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder debug() {
+			super.debug();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder debug(boolean value) {
+			super.debug(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder debugOutputLines(int value) {
+			super.debugOutputLines(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder dictionaryOn(Class<?> on, java.lang.Class<?>...values) {
+			super.dictionaryOn(on, values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder disableBeansRequireSomeProperties() {
+			super.disableBeansRequireSomeProperties();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder disableIgnoreMissingSetters() {
+			super.disableIgnoreMissingSetters();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder disableIgnoreTransientFields() {
+			super.disableIgnoreTransientFields();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder disableIgnoreUnknownNullBeanProperties() {
+			super.disableIgnoreUnknownNullBeanProperties();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder disableInterfaceProxies() {
+			super.disableInterfaceProxies();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public <T> Builder example(Class<T> pojoClass, String json) {
+			super.example(pojoClass, json);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public <T> Builder example(Class<T> pojoClass, T o) {
+			super.example(pojoClass, o);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder fileCharset(Charset value) {
+			super.fileCharset(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder findFluentSetters() {
+			super.findFluentSetters();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder findFluentSetters(Class<?> on) {
+			super.findFluentSetters(on);
+			return this;
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public HashKey hashKey() {
+			return HashKey.of(super.hashKey());
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ignoreInvocationExceptionsOnGetters() {
+			super.ignoreInvocationExceptionsOnGetters();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ignoreInvocationExceptionsOnSetters() {
+			super.ignoreInvocationExceptionsOnSetters();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ignoreUnknownBeanProperties() {
+			super.ignoreUnknownBeanProperties();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ignoreUnknownEnumValues() {
+			super.ignoreUnknownEnumValues();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder impl(Context value) {
+			super.impl(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder implClass(Class<?> interfaceClass, Class<?> implClass) {
+			super.implClass(interfaceClass, implClass);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder implClasses(Map<Class<?>,Class<?>> values) {
+			super.implClasses(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder interfaceClass(Class<?> on, Class<?> value) {
+			super.interfaceClass(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder interfaces(java.lang.Class<?>...value) {
+			super.interfaces(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder listener(Class<? extends org.apache.juneau.parser.ParserListener> value) {
+			super.listener(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder locale(Locale value) {
+			super.locale(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder mediaType(MediaType value) {
+			super.mediaType(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder notBeanClasses(java.lang.Class<?>...values) {
+			super.notBeanClasses(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder notBeanPackages(String...values) {
+			super.notBeanPackages(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder propertyNamer(Class<?> on, Class<? extends org.apache.juneau.PropertyNamer> value) {
+			super.propertyNamer(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder propertyNamer(Class<? extends org.apache.juneau.PropertyNamer> value) {
+			super.propertyNamer(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder sortProperties() {
+			super.sortProperties();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder sortProperties(java.lang.Class<?>...on) {
+			super.sortProperties(on);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder stopClass(Class<?> on, Class<?> value) {
+			super.stopClass(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder streamCharset(Charset value) {
+			super.streamCharset(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder strict() {
+			super.strict();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder strict(boolean value) {
+			super.strict(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public <T,S> Builder swap(Class<T> normalClass, Class<S> swappedClass, ThrowingFunction<T,S> swapFunction) {
+			super.swap(normalClass, swappedClass, swapFunction);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public <T,S> Builder swap(Class<T> normalClass, Class<S> swappedClass, ThrowingFunction<T,S> swapFunction, ThrowingFunction<S,T> unswapFunction) {
+			super.swap(normalClass, swappedClass, swapFunction, unswapFunction);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder swaps(Class<?>...values) {
+			super.swaps(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder swaps(Object...values) {
+			super.swaps(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder timeZone(TimeZone value) {
+			super.timeZone(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder trimStrings() {
+			super.trimStrings();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder trimStrings(boolean value) {
+			super.trimStrings(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder type(Class<? extends org.apache.juneau.Context> value) {
+			super.type(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder typeName(Class<?> on, String value) {
+			super.typeName(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder typePropertyName(Class<?> on, String value) {
+			super.typePropertyName(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder typePropertyName(String value) {
+			super.typePropertyName(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder unbuffered() {
+			super.unbuffered();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder unbuffered(boolean value) {
+			super.unbuffered(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder useEnumNames() {
+			super.useEnumNames();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder useJavaBeanIntrospector() {
+			super.useJavaBeanIntrospector();
+			return this;
+		}
+	}
+
+	/** Default parser, all default settings.*/
+	public static final YamlParser DEFAULT = new YamlParser(create());
+
+	/**
+	 * Creates a new builder for this object.
+	 *
+	 * @return A new builder.
+	 */
+	public static Builder create() {
+		return new Builder();
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param builder The builder for this object.
+	 */
+	public YamlParser(Builder builder) {
+		super(builder);
+	}
+
+	@Override /* Overridden from Context */
+	public Builder copy() {
+		return new Builder(this);
+	}
+
+	@Override /* Overridden from Context */
+	public YamlParserSession.Builder createSession() {
+		return YamlParserSession.create(this);
+	}
+
+	@Override /* Overridden from Context */
+	public YamlParserSession getSession() { return createSession().build(); }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParserSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParserSession.java
new file mode 100644
index 0000000000..10633d0133
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParserSession.java
@@ -0,0 +1,1456 @@
+/*
+ * 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.yaml;
+
+import static org.apache.juneau.commons.lang.StateEnum.*;
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.commons.utils.StringUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.function.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.commons.lang.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.commons.utils.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
+
+/**
+ * Session object that lives for the duration of a single use of {@link YamlParser}.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * 	<li class='warn'>This class is not thread safe and is typically discarded after one use.
+ * </ul>
+ */
+@SuppressWarnings({
+	"java:S125",  // State-machine and parse-path comments are documentation, not commented-out code
+	"unchecked",
+	"rawtypes",
+	"resource"
+})
+public class YamlParserSession extends ReaderParserSession {
+
+	/**
+	 * Builder class.
+	 */
+	public static class Builder extends ReaderParserSession.Builder {
+
+		private YamlParser ctx;
+
+		/**
+		 * Constructor
+		 *
+		 * @param ctx The context creating this session.
+		 */
+		protected Builder(YamlParser ctx) {
+			super(ctx);
+			this.ctx = ctx;
+		}
+
+		@Override /* Overridden from Builder */
+		public <T> Builder apply(Class<T> type, Consumer<T> apply) {
+			super.apply(type, apply);
+			return this;
+		}
+
+		@Override
+		public YamlParserSession build() {
+			return new YamlParserSession(this);
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder debug(Boolean value) {
+			super.debug(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder fileCharset(Charset value) {
+			super.fileCharset(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder javaMethod(Method value) {
+			super.javaMethod(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder locale(Locale value) {
+			super.locale(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder mediaType(MediaType value) {
+			super.mediaType(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder mediaTypeDefault(MediaType value) {
+			super.mediaTypeDefault(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder outer(Object value) {
+			super.outer(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder properties(Map<String,Object> value) {
+			super.properties(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder property(String key, Object value) {
+			super.property(key, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder schema(HttpPartSchema value) {
+			super.schema(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder schemaDefault(HttpPartSchema value) {
+			super.schemaDefault(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder streamCharset(Charset value) {
+			super.streamCharset(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder timeZone(TimeZone value) {
+			super.timeZone(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder timeZoneDefault(TimeZone value) {
+			super.timeZoneDefault(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder unmodifiable() {
+			super.unmodifiable();
+			return this;
+		}
+	}
+
+	/**
+	 * Creates a new builder for this object.
+	 *
+	 * @param ctx The context creating this session.
+	 * @return A new builder.
+	 */
+	public static Builder create(YamlParser ctx) {
+		return new Builder(ctx);
+	}
+
+	private final YamlParser ctx;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param builder The builder for this object.
+	 */
+	protected YamlParserSession(Builder builder) {
+		super(builder);
+		ctx = builder.ctx;
+	}
+
+	@Override /* Overridden from ParserSession */
+	protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
+		try (var r = pipe.getParserReader()) {
+			if (r == null)
+				return null;
+			return parseAnything(type, r, getOuter(), null);
+		}
+	}
+
+	@Override /* Overridden from ReaderParserSession */
+	protected <E> Collection<E> doParseIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws IOException, ParseException, ExecutableException {
+		try (var r = pipe.getParserReader()) {
+			skipWhitespaceAndComments(r);
+			int ch = r.peek();
+			if (ch == '[') {
+				parseFlowSequence(r, c, getClassMeta(elementType), null);
+			} else {
+				parseBlockSequence(r, c, getClassMeta(elementType), null, 0);
+			}
+			return c;
+		}
+	}
+
+	@Override /* Overridden from ReaderParserSession */
+	protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws IOException, ParseException, ExecutableException {
+		try (var r = pipe.getParserReader()) {
+			skipWhitespaceAndComments(r);
+			int ch = r.peek();
+			if (ch == '{') {
+				parseFlowMapping(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null);
+			} else {
+				parseBlockMapping(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null, 0);
+			}
+			return m;
+		}
+	}
+
+	@SuppressWarnings({
+		"java:S3776" // Cognitive complexity acceptable for parser dispatch
+	})
+	private <T> T parseAnything(ClassMeta<?> eType, ParserReader r, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
+
+		if (eType == null)
+			eType = object();
+		var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
+		var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
+		ClassMeta<?> sType = null;
+		if (nn(builder))
+			sType = builder.getBuilderClassMeta(this);
+		else if (nn(swap))
+			sType = swap.getSwapClassMeta(this);
+		else
+			sType = eType;
+
+		if (sType.isOptional())
+			return (T)opt(parseAnything(eType.getElementType(), r, outer, pMeta));
+
+		setCurrentClass(sType);
+
+		Object o = null;
+
+		skipWhitespaceAndComments(r);
+		skipDocumentMarker(r);
+		skipWhitespaceAndComments(r);
+
+		int contentColumn = r.getPosition().getColumn();
+		int c = r.peek();
+		if (c == -1) {
+			// Let o be null.
+		} else if (c == '{') {
+			if (sType.isObject()) {
+				var m2 = new JsonMap(this);
+				parseFlowMapping(r, m2, string(), object(), pMeta);
+				o = cast(m2, pMeta, eType);
+			} else if (sType.isMap()) {
+				Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
+				o = parseFlowMapping(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
+			} else if (sType.isCollection()) {
+				var m = new JsonMap(this);
+				parseFlowMapping(r, m, string(), object(), pMeta);
+				o = cast(m, pMeta, eType);
+			} else if (nn(builder)) {
+				var m = toBeanMap(builder.create(this, eType));
+				o = builder.build(this, parseIntoBeanMap(r, m).getBean(), eType);
+			} else if (sType.canCreateNewBean(outer)) {
+				var m = newBeanMap(outer, sType.inner());
+				o = parseIntoBeanMap(r, m).getBean();
+			} else if (sType.isArray() || sType.isArgs()) {
+				var m = new JsonMap(this);
+				parseFlowMapping(r, m, string(), object(), pMeta);
+				o = cast(m, pMeta, eType);
+			} else {
+				Map m = new JsonMap(this);
+				parseFlowMapping(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
+				if (m.containsKey(getBeanTypePropertyName(eType)))
+					o = cast((JsonMap)m, pMeta, eType);
+				else if (nn(sType.getProxyInvocationHandler()))
+					o = newBeanMap(outer, sType.inner()).load(m).getBean();
+				else
+					throw new ParseException(this, "Class ''{0}'' could not be instantiated.  Reason: ''{1}''", cn(sType), sType.getNotABeanReason());
+			}
+		} else if (c == '[') {
+			if (sType.isObject()) {
+				o = parseFlowSequence(r, new JsonList(this), object(), pMeta);
+			} else if (sType.isCollection()) {
+				Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : new JsonList(this));
+				o = parseFlowSequence(r, l, sType, pMeta);
+			} else if (sType.isArray() || sType.isArgs()) {
+				var l = (ArrayList)parseFlowSequence(r, list(), sType, pMeta);
+				o = toArray(sType, l);
+			} else {
+				throw new ParseException(this, "Unrecognized syntax for class type ''{0}'', starting character ''{1}''", sType, (char)c);
+			}
+		} else if (c == '\'') {
+			String s = parseSingleQuotedString(r);
+			o = handleQuotedScalar(s, r, sType, eType, builder, outer, pMeta, contentColumn > 0 ? contentColumn - 1 : 0);
+		} else if (c == '"') {
+			String s = parseDoubleQuotedString(r);
+			o = handleQuotedScalar(s, r, sType, eType, builder, outer, pMeta, contentColumn > 0 ? contentColumn - 1 : 0);
+		} else if (c == '-') {
+			int c2 = peekSecondChar(r);
+			if (c2 == ' ' || c2 == '\n' || c2 == '\r') {
+				if (sType.isObject()) {
+					o = parseBlockSequence(r, new JsonList(this), object(), pMeta, 0);
+				} else if (sType.isCollection()) {
+					Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : new JsonList(this));
+					o = parseBlockSequence(r, l, sType, pMeta, 0);
+				} else if (sType.isArray() || sType.isArgs()) {
+					var l = (ArrayList)parseBlockSequence(r, list(), sType, pMeta, 0);
+					o = toArray(sType, l);
+				} else {
+					throw new ParseException(this, "Unrecognized syntax for class type ''{0}'', starting character ''{1}''", sType, (char)c);
+				}
+			} else {
+				String s = parsePlainScalar(r, 0);
+				o = convertToType(s, sType, eType, outer, pMeta);
+			}
+		} else if (c == '|' || c == '>') {
+			String s = parseBlockScalar(r, (char)c);
+			o = convertToType(s, sType, eType, outer, pMeta);
+		} else if (c == '~') {
+			r.read(); // consume '~'
+			o = null;
+		} else {
+			String s = parsePlainScalar(r, 0);
+			o = handlePlainScalar(s, r, sType, eType, builder, outer, pMeta, contentColumn > 0 ? contentColumn - 1 : 0);
+		}
+
+		if (nn(swap) && nn(o))
+			o = unswap(swap, o, eType);
+
+		if (nn(outer))
+			setParent(eType, o, outer);
+
+		return (T)o;
+	}
+
+	@SuppressWarnings("rawtypes")
+	private <T> Object handleQuotedScalar(String s, ParserReader r, ClassMeta<?> sType, ClassMeta<?> eType, BuilderSwap<T,Object> builder, Object outer, BeanPropertyMeta pMeta, int keyIndent) throws IOException, ParseException, ExecutableException {
+		if (looksLikeMappingKey(s, r)) {
+			r.read(); // consume ':'
+			int cp = r.peek();
+			if (cp == ' ')
+				r.read();
+
+			if (sType.isObject()) {
+				var m2 = new JsonMap(this);
+				Object value = parseAnything(object(), r, m2, pMeta);
+				String ts = trim(s);
+				setName(object(), value, ts);
+				m2.put(ts, value);
+				parseBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
+				return cast(m2, pMeta, eType);
+			} else if (sType.isMap()) {
+				Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
+				Object value = parseAnything(sType.getValueType(), r, m, pMeta);
+				Object key = convertAttrToType(m, trim(s), sType.getKeyType());
+				setName(sType.getValueType(), value, key);
+				m.put(key, value);
+				parseBlockMappingRemainder(r, m, sType.getKeyType(), sType.getValueType(), pMeta, keyIndent);
+				return m;
+			} else if (nn(builder)) {
+				var m = toBeanMap(builder.create(this, eType));
+				parseBeanProperty(r, m, s);
+				parseIntoBeanMapBlockRemainder(r, m, keyIndent);
+				return builder.build(this, m.getBean(), eType);
+			} else if (sType.canCreateNewBean(outer)) {
+				var m = newBeanMap(outer, sType.inner());
+				parseBeanProperty(r, m, s);
+				parseIntoBeanMapBlockRemainder(r, m, keyIndent);
+				return m.getBean();
+			} else {
+				var m2 = new JsonMap(this);
+				Object value = parseAnything(object(), r, m2, pMeta);
+				m2.put(s, value);
+				parseBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
+				if (m2.containsKey(getBeanTypePropertyName(eType)))
+					return cast(m2, pMeta, eType);
+				if (nn(sType.getProxyInvocationHandler()))
+					return newBeanMap(outer, sType.inner()).load(m2).getBean();
+				throw new ParseException(this, "Class ''{0}'' could not be instantiated.  Reason: ''{1}''", cn(sType), sType.getNotABeanReason());
+			}
+		}
+		if (sType.isObject())
+			return trim(s);
+		return convertToType(s, sType, eType, outer, pMeta);
+	}
+
+	@SuppressWarnings("rawtypes")
+	private <T> Object handlePlainScalar(String s, ParserReader r, ClassMeta<?> sType, ClassMeta<?> eType, BuilderSwap<T,Object> builder, Object outer, BeanPropertyMeta pMeta, int keyIndent) throws IOException, ParseException, ExecutableException {
+		if (s.isEmpty())
+			return null;
+
+		if ("null".equals(s))
+			return null;
+
+		if ("true".equals(s) || "false".equals(s)) {
+			if (sType.isObject() || sType.isBoolean())
+				return Boolean.valueOf(s);
+			return convertToType(s, sType, eType, outer, pMeta);
+		}
+
+		if (looksLikeMappingKey(s, r)) {
+			r.read(); // consume ':'
+			int cp = r.peek();
+			if (cp == ' ')
+				r.read(); // consume space after ':'
+
+			if (sType.isObject()) {
+				var m2 = new JsonMap(this);
+				Object value = parseAnything(object(), r, m2, pMeta);
+				String ts = trim(s);
+				setName(object(), value, ts);
+				m2.put(ts, value);
+				parseBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
+				return cast(m2, pMeta, eType);
+			} else if (sType.isMap()) {
+				Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
+				Object value = parseAnything(sType.getValueType(), r, m, pMeta);
+				Object key = convertAttrToType(m, trim(s), sType.getKeyType());
+				setName(sType.getValueType(), value, key);
+				m.put(key, value);
+				parseBlockMappingRemainder(r, m, sType.getKeyType(), sType.getValueType(), pMeta, keyIndent);
+				return m;
+			} else if (nn(builder)) {
+				var m = toBeanMap(builder.create(this, eType));
+				parseBeanProperty(r, m, s);
+				parseIntoBeanMapBlockRemainder(r, m, keyIndent);
+				return builder.build(this, m.getBean(), eType);
+			} else if (sType.canCreateNewBean(outer)) {
+				var m = newBeanMap(outer, sType.inner());
+				parseBeanProperty(r, m, s);
+				parseIntoBeanMapBlockRemainder(r, m, keyIndent);
+				return m.getBean();
+			} else {
+				var m2 = new JsonMap(this);
+				Object value = parseAnything(object(), r, m2, pMeta);
+				m2.put(s, value);
+				parseBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
+				if (m2.containsKey(getBeanTypePropertyName(eType)))
+					return cast(m2, pMeta, eType);
+				if (nn(sType.getProxyInvocationHandler()))
+					return newBeanMap(outer, sType.inner()).load(m2).getBean();
+				throw new ParseException(this, "Class ''{0}'' could not be instantiated.  Reason: ''{1}''", cn(sType), sType.getNotABeanReason());
+			}
+		}
+
+		return convertToType(s, sType, eType, outer, pMeta);
+	}
+
+	private Object convertToType(String s, ClassMeta<?> sType, ClassMeta<?> eType, Object outer, BeanPropertyMeta pMeta) throws ParseException {
+		if (sType.isObject()) {
+			return resolveScalarType(trim(s));
+		} else if (sType.isBoolean()) {
+			return bool(s);
+		} else if (sType.isCharSequence()) {
+			return trim(s);
+		} else if (sType.isChar()) {
+			return parseCharacter(s);
+		} else if (sType.isNumber()) {
+			return StringUtils.parseNumber(s, (Class<? extends Number>)sType.inner());
+		} else if (sType.isDateOrCalendarOrTemporal() || sType.isDuration()) {
+			return Iso8601Utils.parse(s, sType, getTimeZone());
+		} else if (sType.canCreateNewInstanceFromString(outer)) {
+			return sType.newInstanceFromString(outer, s);
+		} else {
+			throw new ParseException(this, "Unrecognized syntax for class type ''{0}'', value ''{1}''", sType, s);
+		}
+	}
+
+	private boolean looksLikeMappingKey(String s, ParserReader r) throws IOException {
+		int c = r.peek();
+		if (c != ':')
+			return false;
+		// Peek past ':' to make sure it's followed by space, newline, or EOF (not part of value)
+		return true;
+	}
+
+	private <K,V> void parseBlockMappingRemainder(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta, int parentIndent) throws IOException, ParseException, ExecutableException {
+		parseBlockMapping(r, m, keyType, valueType, pMeta, parentIndent);
+	}
+
+	private <T> void parseIntoBeanMapBlockRemainder(ParserReader r, BeanMap<T> m, int parentIndent) throws IOException, ParseException, ExecutableException {
+		parseIntoBeanMapBlock(r, m, parentIndent);
+	}
+
+	private <T> void parseBeanProperty(ParserReader r, BeanMap<T> m, String currAttr) throws IOException, ParseException, ExecutableException {
+		var pm = m.getPropertyMeta(currAttr);
+		setCurrentProperty(pm);
+		if (pm == null) {
+			onUnknownProperty(currAttr, m, parseAnything(object(), r, m.getBean(false), null));
+		} else {
+			var cm = pm.getClassMeta();
+			Object value = parseAnything(cm, r, m.getBean(false), pm);
+			setName(cm, value, currAttr);
+			try {
+				pm.set(m, currAttr, value);
+			} catch (BeanRuntimeException e) {
+				onBeanSetterException(pm, e);
+				throw e;
+			}
+		}
+		setCurrentProperty(null);
+	}
+
+	private boolean isNullBlockValue(ParserReader r, int blockIndent) throws IOException {
+		int c = r.peek();
+		if (c == -1)
+			return true;
+		if (c != '\n' && c != '\r')
+			return false;
+		int charsRead = 0;
+		while (true) {
+			c = r.read();
+			if (c == -1) {
+				for (int j = 0; j < charsRead; j++) r.unread();
+				return true;
+			}
+			charsRead++;
+			if (c != '\n' && c != '\r' && c != ' ' && c != '\t')
+				break;
+		}
+		int nextColumn = r.getPosition().getColumn() - 1;
+		for (int j = 0; j < charsRead; j++) r.unread();
+		return nextColumn <= blockIndent;
+	}
+
+	private int peekSecondChar(ParserReader r) throws IOException {
+		r.read();
+		int c2 = r.peek();
+		r.unread();
+		return c2;
+	}
+
+	// ==========================================
+	// Flow mapping: { key: value, ... }
+	// ==========================================
+	@SuppressWarnings({
+		"java:S1168",
+		"java:S135",
+		"java:S2583",
+		"java:S3776"
+	})
+	private <K,V> Map<K,V> parseFlowMapping(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
+
+		if (keyType == null)
+			keyType = (ClassMeta<K>)string();
+
+		// S1: Looking for outer {
+		// S2: Looking for key start or }
+		// S3: Found key, looking for :
+		// S4: Found :, looking for value
+		// S5: Looking for , or }
+		// S6: Found , looking for key start
+
+		var state = S1;
+		String currKey = null;
+		int c = 0;
+		while (c != -1) {
+			c = r.read();
+			if (state == S1) {
+				if (c == '{')
+					state = S2;
+				else if (isWhitespace(c))
+					continue;
+				else
+					break;
+			} else if (state == S2) {
+				if (c == '}') {
+					return m;
+				} else if (isWhitespace(c)) {
+					continue;
+				} else {
+					r.unread();
+					currKey = parseFlowMappingKey(r);
+					state = S3;
+				}
+			} else if (state == S3) {
+				if (c == ':')
+					state = S4;
+				else if (isWhitespace(c))
+					continue;
+			} else if (state == S4) {
+				if (isWhitespace(c)) {
+					continue;
+				} else {
+					r.unread();
+					K key = convertAttrToType(m, currKey, keyType);
+					V value = parseAnything(valueType, r, m, pMeta);
+					setName(valueType, value, key);
+					m.put(key, value);
+					state = S5;
+				}
+			} else if (state == S5) {
+				if (c == ',') {
+					state = S6;
+				} else if (isWhitespace(c)) {
+					continue;
+				} else if (c == '}') {
+					return m;
+				} else {
+					break;
+				}
+			} else if (state == S6) {
+				if (c == '}') {
+					break;
+				} else if (isWhitespace(c)) {
+					continue;
+				} else {
+					r.unread();
+					currKey = parseFlowMappingKey(r);
+					state = S3;
+				}
+			}
+		}
+		if (state == S1)
+			throw new ParseException(this, "Expected '{' at beginning of YAML flow mapping.");
+		if (state == S2)
+			throw new ParseException(this, "Could not find key in YAML flow mapping.");
+		if (state == S3)
+			throw new ParseException(this, "Could not find ':' following key in YAML flow mapping.");
+		if (state == S4)
+			throw new ParseException(this, "Expected value in YAML flow mapping.");
+		if (state == S5)
+			throw new ParseException(this, "Could not find '}' marking end of YAML flow mapping.");
+		if (state == S6)
+			throw new ParseException(this, "Unexpected '}' found in YAML flow mapping.");
+
+		return null; // Unreachable.
+	}
+
+	private String parseFlowMappingKey(ParserReader r) throws IOException, ParseException {
+		int c = r.peek();
+		if (c == '\'' )
+			return parseSingleQuotedString(r);
+		if (c == '"')
+			return parseDoubleQuotedString(r);
+		return parsePlainFlowKey(r);
+	}
+
+	private String parsePlainFlowKey(ParserReader r) throws IOException {
+		var sb = new StringBuilder();
+		int c;
+		while ((c = r.read()) != -1) {
+			if (c == ':' || c == ',' || c == '}' || c == '{' || c == '[' || c == ']') {
+				r.unread();
+				break;
+			}
+			sb.append((char)c);
+		}
+		return sb.toString().trim();
+	}
+
+	// ==========================================
+	// Flow sequence: [ value, ... ]
+	// ==========================================
+	@SuppressWarnings({
+		"java:S1168",
+		"java:S135",
+		"java:S2583",
+		"java:S3776"
+	})
+	private <E> Collection<E> parseFlowSequence(ParserReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
+
+		// S1: Looking for outermost [
+		// S2: Looking for value or ]
+		// S3: Looking for , or ]
+		// S4: Looking for value
+
+		int argIndex = 0;
+
+		var state = S1;
+		int c = 0;
+		while (c != -1) {
+			c = r.read();
+			if (state == S1) {
+				if (c == '[')
+					state = S2;
+				else if (isWhitespace(c))
+					continue;
+				else
+					break;
+			} else if (state == S2) {
+				if (c == ']') {
+					return l;
+				} else if (isWhitespace(c)) {
+					continue;
+				} else if (c != -1) {
+					r.unread();
+					l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r, l, pMeta));
+					state = S3;
+				}
+			} else if (state == S3) {
+				if (c == ',') {
+					state = S4;
+				} else if (isWhitespace(c)) {
+					continue;
+				} else if (c == ']') {
+					return l;
+				} else {
+					break;
+				}
+			} else if (state == S4) {
+				if (isWhitespace(c)) {
+					continue;
+				} else if (c == ']') {
+					break;
+				} else if (c != -1) {
+					r.unread();
+					l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r, l, pMeta));
+					state = S3;
+				}
+			}
+		}
+		if (state == S1)
+			throw new ParseException(this, "Expected '[' at beginning of YAML flow sequence.");
+		if (state == S2)
+			throw new ParseException(this, "Expected value or ']' in YAML flow sequence.");
+		if (state == S3)
+			throw new ParseException(this, "Expected ',' or ']' in YAML flow sequence.");
+		if (state == S4)
+			throw new ParseException(this, "Unexpected trailing comma in YAML flow sequence.");
+
+		return null; // Unreachable.
+	}
+
+	// ==========================================
+	// Block mapping
+	// ==========================================
+	private <K,V> Map<K,V> parseBlockMapping(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta, int parentIndent) throws IOException, ParseException, ExecutableException {
+
+		if (keyType == null)
+			keyType = (ClassMeta<K>)string();
+
+		int blockIndent = -1;
+
+		while (true) {
+			int consumed = skipBlanksAndCountIndent(r);
+			int lineIndent = r.getPosition().getColumn();
+			int c = r.peek();
+			if (c == -1)
+				break;
+
+			if (blockIndent == -1) {
+				if (lineIndent <= parentIndent && parentIndent > 0) {
+					unreadSpaces(r, consumed);
+					break;
+				}
+				blockIndent = lineIndent;
+			}
+
+			if (lineIndent < blockIndent) {
+				unreadSpaces(r, consumed);
+				break;
+			}
+
+			c = r.peek();
+			if (c == '#') {
+				skipToEndOfLine(r);
+				continue;
+			}
+
+			String keyStr = trim(parseBlockMappingKey(r));
+
+			c = r.read(); // Should be ':'
+			if (c != ':')
+				throw new ParseException(this, "Expected ':' after key in YAML block mapping, found ''{0}''", (char)c);
+
+			c = r.peek();
+			if (c == ' ')
+				r.read();
+			K key = convertAttrToType(m, keyStr, keyType);
+			V value = (V)parseAnything(valueType, r, m, pMeta);
+			setName(valueType, value, key);
+			m.put(key, value);
+		}
+
+		return m;
+	}
+
+	private int skipBlanksAndCountIndent(ParserReader r) throws IOException {
+		int indent = 0;
+		int c;
+		while ((c = r.read()) != -1) {
+			if (c == ' ') {
+				indent++;
+			} else if (c == '\n' || c == '\r') {
+				indent = 0;
+			} else if (c == '\t') {
+				indent++;
+			} else {
+				r.unread();
+				break;
+			}
+		}
+		return indent;
+	}
+
+	private void unreadSpaces(ParserReader r, int count) throws IOException {
+		for (int i = 0; i < count; i++)
+			r.unread();
+	}
+
+	private String parseBlockMappingKey(ParserReader r) throws IOException, ParseException {
+		int c = r.peek();
+		if (c == '\'')
+			return parseSingleQuotedString(r);
+		if (c == '"')
+			return parseDoubleQuotedString(r);
+		var sb = new StringBuilder();
+		while ((c = r.read()) != -1) {
+			if (c == ':') {
+				int next = r.peek();
+				if (next == ' ' || next == '\n' || next == '\r' || next == -1) {
+					r.unread();
+					break;
+				}
+				sb.append((char)c);
+			} else if (c == '\n' || c == '\r') {
+				r.unread();
+				break;
+			} else {
+				sb.append((char)c);
+			}
+		}
+		String key = sb.toString().trim();
+		if ("null".equals(key) || "Null".equals(key) || "NULL".equals(key) || "~".equals(key))
+			return null;
+		return key;
+	}
+
+	// ==========================================
+	// Block mapping for beans
+	// ==========================================
+	@SuppressWarnings({
+		"java:S3776"
+	})
+	private <T> BeanMap<T> parseIntoBeanMap(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
+		int c = r.peek();
+		if (c == '{') {
+			return parseIntoBeanMapFlow(r, m);
+		}
+		return parseIntoBeanMapBlock(r, m, 0);
+	}
+
+	@SuppressWarnings({
+		"java:S1168",
+		"java:S2583",
+		"java:S3776"
+	})
+	private <T> BeanMap<T> parseIntoBeanMapFlow(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
+
+		// S1: Looking for outer {
+		// S2: Looking for attrName start.
+		// S3: Found attrName end, looking for :.
+		// S4: Found :, looking for valStart.
+		// S5: Looking for , or }
+
+		var state = S1;
+		var currAttr = "";
+		int c = 0;
+		mark();
+		try {
+			while (c != -1) {
+				c = r.read();
+				if (state == S1) {
+					if (c == '{')
+						state = S2;
+					else if (isWhitespace(c))
+						continue;
+					else
+						break;
+				} else if (state == S2) {
+					if (c == '}') {
+						return m;
+					} else if (isWhitespace(c)) {
+						continue;
+					} else {
+						r.unread();
+						mark();
+						currAttr = parseFlowMappingKey(r);
+						state = S3;
+					}
+				} else if (state == S3) {
+					if (c == ':')
+						state = S4;
+					else if (isWhitespace(c))
+						continue;
+				} else if (state == S4) {
+					if (isWhitespace(c)) {
+						continue;
+					} else {
+						if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
+							var pm = m.getPropertyMeta(currAttr);
+							setCurrentProperty(pm);
+							if (pm == null) {
+								onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), m.getBean(false), null));
+								unmark();
+							} else {
+								unmark();
+								var cm = pm.getClassMeta();
+								Object value = parseAnything(cm, r.unread(), m.getBean(false), pm);
+								setName(cm, value, currAttr);
+								try {
+									pm.set(m, currAttr, value);
+								} catch (BeanRuntimeException e) {
+									onBeanSetterException(pm, e);
+									throw e;
+								}
+							}
+							setCurrentProperty(null);
+						}
+						state = S5;
+					}
+				} else if (state == S5) {
+					if (c == ',')
+						state = S2;
+					else if (isWhitespace(c))
+						continue;
+					else if (c == '}') {
+						return m;
+					}
+				}
+			}
+			if (state == S1)
+				throw new ParseException(this, "Expected '{' at beginning of YAML flow mapping.");
+			if (state == S2)
+				throw new ParseException(this, "Could not find attribute name in YAML flow mapping.");
+			if (state == S3)
+				throw new ParseException(this, "Could not find ':' following attribute name in YAML flow mapping.");
+			if (state == S4)
+				throw new ParseException(this, "Expected value in YAML flow mapping.");
+			if (state == S5)
+				throw new ParseException(this, "Could not find '}' marking end of YAML flow mapping.");
+		} finally {
+			unmark();
+		}
+
+		return null; // Unreachable.
+	}
+
+	@SuppressWarnings({
+		"java:S3776"
+	})
+	private <T> BeanMap<T> parseIntoBeanMapBlock(ParserReader r, BeanMap<T> m, int parentIndent) throws IOException, ParseException, ExecutableException {
+
+		int blockIndent = -1;
+
+		mark();
+		try {
+			while (true) {
+				int consumed = skipBlanksAndCountIndent(r);
+				int lineIndent = r.getPosition().getColumn();
+				int c = r.peek();
+				if (c == -1)
+					break;
+
+				if (blockIndent == -1) {
+					if (lineIndent <= parentIndent && parentIndent > 0) {
+						unreadSpaces(r, consumed);
+						break;
+					}
+					blockIndent = lineIndent;
+				}
+
+				if (lineIndent < blockIndent) {
+					unreadSpaces(r, consumed);
+					break;
+				}
+
+				c = r.peek();
+				if (c == '#') {
+					skipToEndOfLine(r);
+					continue;
+				}
+
+				String currAttr = trim(parseBlockMappingKey(r));
+
+				c = r.read(); // Should be ':'
+				if (c != ':')
+					throw new ParseException(this, "Expected ':' after attribute name in YAML block mapping, found ''{0}''", (char)c);
+
+				if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
+					var pm = m.getPropertyMeta(currAttr);
+					setCurrentProperty(pm);
+					if (pm == null) {
+						c = r.peek();
+						if (c == ' ')
+							r.read();
+						Object v = isNullBlockValue(r, blockIndent) ? null : parseAnything(object(), r, m.getBean(false), null);
+						onUnknownProperty(currAttr, m, v);
+						unmark();
+					} else {
+						unmark();
+						var cm = pm.getClassMeta();
+						c = r.peek();
+						if (c == ' ')
+							r.read();
+						Object value = isNullBlockValue(r, blockIndent) ? null : parseAnything(cm, r, m.getBean(false), pm);
+						setName(cm, value, currAttr);
+						try {
+							pm.set(m, currAttr, value);
+						} catch (BeanRuntimeException e) {
+							onBeanSetterException(pm, e);
+							throw e;
+						}
+					}
+					setCurrentProperty(null);
+				}
+				mark();
+			}
+		} finally {
+			unmark();
+		}
+
+		return m;
+	}
+
+	// ==========================================
+	// Block sequence: - value\n- value\n...
+	// ==========================================
+	private <E> Collection<E> parseBlockSequence(ParserReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta, int parentIndent) throws IOException, ParseException, ExecutableException {
+
+		int blockIndent = -1;
+		int argIndex = 0;
+
+		while (true) {
+			int consumed = skipBlanksAndCountIndent(r);
+			int lineIndent = r.getPosition().getColumn();
+			int c = r.peek();
+			if (c == -1)
+				break;
+
+			if (blockIndent == -1) {
+				if (lineIndent <= parentIndent && parentIndent > 0) {
+					unreadSpaces(r, consumed);
+					break;
+				}
+				blockIndent = lineIndent;
+			}
+
+			if (lineIndent < blockIndent) {
+				unreadSpaces(r, consumed);
+				break;
+			}
+
+			c = r.peek();
+			if (c == '#') {
+				skipToEndOfLine(r);
+				continue;
+			}
+			if (c != '-') {
+				unreadSpaces(r, lineIndent);
+				break;
+			}
+
+			r.read(); // consume '-'
+			c = r.peek();
+			if (c != ' ' && c != '\n' && c != '\r') {
+				unreadSpaces(r, lineIndent);
+				break;
+			}
+
+			if (c == ' ')
+				r.read(); // consume space after '-'
+
+			ClassMeta<?> elementType = type.isArgs() ? type.getArg(argIndex++) : type.getElementType();
+			l.add((E)parseAnything(elementType, r, l, pMeta));
+		}
+
+		return l;
+	}
+
+	// ==========================================
+	// Single-quoted string: 'hello ''world'''
+	// ==========================================
+	private String parseSingleQuotedString(ParserReader r) throws IOException, ParseException {
+		int c = r.read(); // consume opening '
+		if (c != '\'')
+			throw new ParseException(this, "Expected single quote to start string.");
+
+		var sb = new StringBuilder();
+		while ((c = r.read()) != -1) {
+			if (c == '\'') {
+				int next = r.peek();
+				if (next == '\'') {
+					r.read(); // consume escaped '
+					sb.append('\'');
+				} else {
+					return sb.toString();
+				}
+			} else {
+				sb.append((char)c);
+			}
+		}
+		throw new ParseException(this, "Could not find end of single-quoted YAML string.");
+	}
+
+	// ==========================================
+	// Double-quoted string: "hello \"world\"\n"
+	// ==========================================
+	@SuppressWarnings({
+		"java:S3776"
+	})
+	private String parseDoubleQuotedString(ParserReader r) throws IOException, ParseException {
+		int c = r.read(); // consume opening "
+		if (c != '"')
+			throw new ParseException(this, "Expected double quote to start string.");
+
+		var sb = new StringBuilder();
+		while ((c = r.read()) != -1) {
+			if (c == '\\') {
+				c = r.read();
+				// @formatter:off
+				switch (c) {
+					case '\\': sb.append('\\'); break;
+					case '"': sb.append('"'); break;
+					case 'n': sb.append('\n'); break;
+					case 't': sb.append('\t'); break;
+					case 'r': sb.append('\r'); break;
+					case '0': sb.append('\0'); break;
+					case 'a': sb.append('\u0007'); break;
+					case 'b': sb.append('\b'); break;
+					case 'f': sb.append('\f'); break;
+					case 'e': sb.append('\u001b'); break;
+					case '/': sb.append('/'); break;
+					case 'x': {
+						String hex = r.read(2);
+						try {
+							sb.append((char)Integer.parseInt(hex, 16));
+						} catch (@SuppressWarnings("unused") NumberFormatException e) {
+							throw new ParseException(this, "Invalid \\x escape sequence in YAML string.");
+						}
+						break;
+					}
+					case 'u': {
+						String hex = r.read(4);
+						try {
+							sb.append((char)Integer.parseInt(hex, 16));
+						} catch (@SuppressWarnings("unused") NumberFormatException e) {
+							throw new ParseException(this, "Invalid \\u escape sequence in YAML string.");
+						}
+						break;
+					}
+					default:
+						throw new ParseException(this, "Invalid escape sequence '\\{0}' in YAML string.", (char)c);
+				}
+				// @formatter:on
+			} else if (c == '"') {
+				return sb.toString();
+			} else {
+				sb.append((char)c);
+			}
+		}
+		throw new ParseException(this, "Could not find end of double-quoted YAML string.");
+	}
+
+	// ==========================================
+	// Plain scalar (unquoted)
+	// ==========================================
+	private String parsePlainScalar(ParserReader r, int indent) throws IOException {
+		var sb = new StringBuilder();
+		int c;
+		while ((c = r.read()) != -1) {
+			if (c == '\n' || c == '\r') {
+				r.unread();
+				break;
+			}
+			if (c == ':') {
+				int next = r.peek();
+				if (next == ' ' || next == '\n' || next == '\r' || next == -1) {
+					r.unread(); // unread ':'
+					break;
+				}
+				sb.append((char)c);
+			} else if (c == ' ') {
+				int next = r.peek();
+				if (next == '#') {
+					// rest of line is comment
+					r.unread();
+					break;
+				}
+				sb.append((char)c);
+			} else if (c == ',' || c == '}' || c == ']') {
+				r.unread();
+				break;
+			} else if (c == '#' && sb.isEmpty()) {
+				r.unread();
+				break;
+			} else {
+				sb.append((char)c);
+			}
+		}
+		String result = sb.toString();
+		// Trim trailing whitespace
+		int end = result.length();
+		while (end > 0 && result.charAt(end - 1) == ' ')
+			end--;
+		return result.substring(0, end);
+	}
+
+	// ==========================================
+	// Block scalar: | or >
+	// ==========================================
+	@SuppressWarnings({
+		"java:S3776"
+	})
+	private String parseBlockScalar(ParserReader r, char indicator) throws IOException, ParseException {
+		r.read(); // consume '|' or '>'
+
+		// Parse indicator line for chomping
+		char chomping = ' '; // default = clip
+		int c;
+		while ((c = r.read()) != -1) {
+			if (c == '-') {
+				chomping = '-'; // strip
+			} else if (c == '+') {
+				chomping = '+'; // keep
+			} else if (c == '\n' || c == '\r') {
+				if (c == '\r') {
+					int next = r.peek();
+					if (next == '\n')
+						r.read();
+				}
+				break;
+			} else if (c >= '1' && c <= '9') {
+				// explicit indent indicator, ignore for now (auto-detect below)
+			} else if (c == ' ' || c == '\t') {
+				continue;
+			} else {
+				throw new ParseException(this, "Unexpected character ''{0}'' in YAML block scalar indicator.", (char)c);
+			}
+		}
+
+		// Read the block content
+		int blockIndent = -1;
+		var lines = new ArrayList<String>();
+		int trailingNewlines = 0;
+
+		while (true) {
+			// Count leading spaces
+			int spaces = 0;
+			while ((c = r.read()) != -1) {
+				if (c == ' ')
+					spaces++;
+				else
+					break;
+			}
+
+			if (c == -1) {
+				break;
+			}
+
+			if (c == '\n' || c == '\r') {
+				// Empty line
+				trailingNewlines++;
+				lines.add("");
+				if (c == '\r' && r.peek() == '\n')
+					r.read();
+				continue;
+			}
+
+			if (blockIndent == -1) {
+				blockIndent = spaces;
+			}
+
+			if (spaces < blockIndent) {
+				// End of block, push back everything
+				r.unread(); // push back non-space char
+				for (int i = 0; i < spaces; i++)
+					r.unread();
+				break;
+			}
+
+			// Read rest of line
+			var lineSb = new StringBuilder();
+			// Include extra spaces beyond blockIndent
+			for (int i = blockIndent; i < spaces; i++)
+				lineSb.append(' ');
+			lineSb.append((char)c);
+			while ((c = r.read()) != -1) {
+				if (c == '\n' || c == '\r') {
+					if (c == '\r' && r.peek() == '\n')
+						r.read();
+					break;
+				}
+				lineSb.append((char)c);
+			}
+
+			// Account for any blank lines that preceded this content line
+			for (int i = 0; i < trailingNewlines; i++)
+				lines.add("");
+			trailingNewlines = 0;
+			lines.add(lineSb.toString());
+
+			if (c == -1)
+				break;
+		}
+
+		// Build the result based on indicator and chomping
+		var sb = new StringBuilder();
+		for (int i = 0; i < lines.size(); i++) {
+			String line = lines.get(i);
+			if (indicator == '|') {
+				// Literal: preserve newlines
+				if (i > 0)
+					sb.append('\n');
+				sb.append(line);
+			} else {
+				// Folded: single newlines -> space, multiple newlines preserved
+				if (i > 0) {
+					if (line.isEmpty() || lines.get(i - 1).isEmpty()) {
+						sb.append('\n');
+					} else {
+						sb.append(' ');
+					}
+				}
+				sb.append(line);
+			}
+		}
+
+		// Apply chomping
+		if (chomping == '-') {
+			// strip: no trailing newline
+		} else if (chomping == '+') {
+			// keep: preserve all trailing newlines
+			sb.append('\n');
+			for (int i = 0; i < trailingNewlines; i++)
+				sb.append('\n');
+		} else {
+			// clip (default): single trailing newline
+			sb.append('\n');
+		}
+
+		return sb.toString();
+	}
+
+	// ==========================================
+	// Helper methods
+	// ==========================================
+
+	private void skipWhitespaceAndComments(ParserReader r) throws IOException {
+		int c;
+		while ((c = r.read()) != -1) {
+			if (c == '#') {
+				// Skip to end of line
+				while ((c = r.read()) != -1) {
+					if (c == '\n' || c == '\r')
+						break;
+				}
+			} else if (!isWhitespace(c)) {
+				r.unread();
+				return;
+			}
+		}
+	}
+
+	private void skipDocumentMarker(ParserReader r) throws IOException {
+		int c = r.peek();
+		if (c == '-') {
+			r.read();
+			int c2 = r.peek();
+			if (c2 == '-') {
+				r.read();
+				int c3 = r.peek();
+				if (c3 == '-') {
+					r.read();
+					// '---' found, skip rest of line
+					skipToEndOfLine(r);
+					return;
+				}
+				r.unread(); // unread second '-'
+			}
+			r.unread(); // unread first '-'
+		} else if (c == '.') {
+			r.read();
+			int c2 = r.peek();
+			if (c2 == '.') {
+				r.read();
+				int c3 = r.peek();
+				if (c3 == '.') {
+					r.read();
+					// '...' found, skip rest of line
+					skipToEndOfLine(r);
+					return;
+				}
+				r.unread();
+			}
+			r.unread();
+		}
+	}
+
+	private void skipToEndOfLine(ParserReader r) throws IOException {
+		int c;
+		while ((c = r.read()) != -1) {
+			if (c == '\n' || c == '\r')
+				return;
+		}
+	}
+
+	private boolean isWhitespace(int c) {
+		return c == ' ' || c == '\t' || c == '\n' || c == '\r';
+	}
+
+	private Object resolveScalarType(String s) {
+		if (s == null || "null".equals(s) || "~".equals(s) || s.isEmpty())
+			return null;
+		if ("true".equals(s) || "True".equals(s) || "TRUE".equals(s))
+			return Boolean.TRUE;
+		if ("false".equals(s) || "False".equals(s) || "FALSE".equals(s))
+			return Boolean.FALSE;
+
+		// Try parsing as number
+		if (!s.isEmpty()) {
+			char first = s.charAt(0);
+			if (first == '-' || first == '+' || (first >= '0' && first <= '9') || first == '.') {
+				try {
+					if (s.contains(".") || s.contains("e") || s.contains("E")) {
+						Double d = Double.parseDouble(s);
+						if (!d.isInfinite() && !d.isNaN())
+							return d;
+					} else {
+						try {
+							return Integer.parseInt(s);
+						} catch (@SuppressWarnings("unused") NumberFormatException e2) {
+							try {
+								return Long.parseLong(s);
+							} catch (@SuppressWarnings("unused") NumberFormatException e3) {
+								// Fall through to string
+							}
+						}
+					}
+				} catch (@SuppressWarnings("unused") NumberFormatException e) {
+					// Fall through to string
+				}
+			}
+		}
+
+		return s;
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
new file mode 100644
index 0000000000..b6c95ac26e
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
@@ -0,0 +1,907 @@
+/*
+ * 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.yaml;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.lang.annotation.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.commons.function.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Serializes POJO models to YAML.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Handles <c>Accept</c> types:  <bc>application/yaml, text/yaml</bc>
+ * <p>
+ * Produces <c>Content-Type</c> types:  <bc>application/yaml</bc>
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * The conversion is as follows...
+ * <ul class='spaced-list'>
+ * 	<li>
+ * 		Maps (e.g. {@link HashMap HashMaps}, {@link TreeMap TreeMaps}) are converted to YAML mappings.
+ * 	<li>
+ * 		Collections (e.g. {@link HashSet HashSets}, {@link LinkedList LinkedLists}) and Java arrays are converted to
+ * 		YAML sequences.
+ * 	<li>
+ * 		{@link String Strings} are converted to YAML scalars.
+ * 	<li>
+ * 		{@link Number Numbers} (e.g. {@link Integer}, {@link Long}, {@link Double}) are converted to YAML numbers.
+ * 	<li>
+ * 		{@link Boolean Booleans} are converted to YAML booleans.
+ * 	<li>
+ * 		{@code nulls} are converted to YAML nulls.
+ * 	<li>
+ * 		{@code arrays} are converted to YAML sequences.
+ * 	<li>
+ * 		{@code beans} are converted to YAML mappings.
+ * </ul>
+ *
+ * <p>
+ * The types above are considered "YAML-primitive" object types.
+ * Any non-YAML-primitive object types are transformed into YAML-primitive object types through
+ * {@link org.apache.juneau.swap.ObjectSwap ObjectSwaps} associated through the
+ * {@link org.apache.juneau.BeanContext.Builder#swaps(Class...)} method.
+ * Several default transforms are provided for transforming Dates, Enums, Iterators, etc...
+ *
+ * <p>
+ * This serializer provides several serialization options.
+ * Typically, one of the predefined DEFAULT serializers will be sufficient.
+ * However, custom serializers can be constructed to fine-tune behavior.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * 	<jc>// Use one of the default serializers to serialize a POJO</jc>
+ * 	String <jv>yaml</jv> = YamlSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>someObject</jv>);
+ *
+ * 	<jc>// Create a custom serializer</jc>
+ * 	YamlSerializer <jv>serializer</jv> = YamlSerializer.<jsm>create</jsm>().build();
+ *
+ * 	<jc>// Clone an existing serializer and modify it to use whitespace</jc>
+ * 	<jv>serializer</jv> = YamlSerializer.<jsf>DEFAULT</jsf>.copy().ws().build();
+ *
+ * 	<jc>// Serialize a POJO to YAML</jc>
+ * 	String <jv>yaml</jv> = <jv>serializer</jv>.serialize(<jv>someObject</jv>);
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * 	<li class='note'>This class is thread safe and reusable.
+ * </ul>
+ */
+@SuppressWarnings({
+	"java:S110", // Inheritance depth acceptable for this class hierarchy
+	"java:S115", // Constants use UPPER_snakeCase naming convention
+})
+public class YamlSerializer extends WriterSerializer {
+
+	private static final String PROP_addBeanTypesYaml = "addBeanTypesYaml";
+
+	private static final String ARG_copyFrom = "copyFrom";
+
+	/**
+	 * Builder class.
+	 */
+	public static class Builder extends WriterSerializer.Builder {
+
+		private static final Cache<HashKey,YamlSerializer> CACHE = Cache.of(HashKey.class, YamlSerializer.class).build();
+
+		private boolean addBeanTypesYaml;
+
+		/**
+		 * Constructor, default settings.
+		 */
+		protected Builder() {
+			produces("application/yaml");
+			accept("application/yaml,text/yaml");
+			addBeanTypesYaml = env("YamlSerializer.addBeanTypes", false);
+		}
+
+		/**
+		 * Copy constructor.
+		 *
+		 * @param copyFrom The builder to copy from.
+		 * 	<br>Cannot be <jk>null</jk>.
+		 */
+		protected Builder(Builder copyFrom) {
+			super(assertArgNotNull(ARG_copyFrom, copyFrom));
+			addBeanTypesYaml = copyFrom.addBeanTypesYaml;
+		}
+
+		/**
+		 * Copy constructor.
+		 *
+		 * @param copyFrom The bean to copy from.
+		 * 	<br>Cannot be <jk>null</jk>.
+		 */
+		protected Builder(YamlSerializer copyFrom) {
+			super(assertArgNotNull(ARG_copyFrom, copyFrom));
+			addBeanTypesYaml = copyFrom.addBeanTypesYaml;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder accept(String value) {
+			super.accept(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder addBeanTypes() {
+			super.addBeanTypes();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder addBeanTypes(boolean value) {
+			super.addBeanTypes(value);
+			return this;
+		}
+
+		/**
+		 * Add <js>"_type"</js> properties when needed.
+		 *
+		 * <p>
+		 * If <jk>true</jk>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred
+		 * through reflection.
+		 *
+		 * <p>
+		 * When present, this value overrides the {@link org.apache.juneau.serializer.Serializer.Builder#addBeanTypes()} setting and is
+		 * provided to customize the behavior of specific serializers in a {@link SerializerSet}.
+		 *
+		 * @return This object.
+		 */
+		public Builder addBeanTypesYaml() {
+			return addBeanTypesYaml(true);
+		}
+
+		/**
+		 * Same as {@link #addBeanTypesYaml()} but allows you to explicitly specify the value.
+		 *
+		 * @param value The value for this setting.
+		 * @return This object.
+		 */
+		public Builder addBeanTypesYaml(boolean value) {
+			addBeanTypesYaml = value;
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder addRootType() {
+			super.addRootType();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder addRootType(boolean value) {
+			super.addRootType(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder annotations(Annotation...values) {
+			super.annotations(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder apply(AnnotationWorkList work) {
+			super.apply(work);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder applyAnnotations(Class<?>...from) {
+			super.applyAnnotations(from);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder applyAnnotations(Object...from) {
+			super.applyAnnotations(from);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanClassVisibility(Visibility value) {
+			super.beanClassVisibility(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanConstructorVisibility(Visibility value) {
+			super.beanConstructorVisibility(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanContext(BeanContext value) {
+			super.beanContext(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanContext(BeanContext.Builder value) {
+			super.beanContext(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanDictionary(java.lang.Class<?>...values) {
+			super.beanDictionary(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanFieldVisibility(Visibility value) {
+			super.beanFieldVisibility(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanInterceptor(Class<?> on, Class<? extends org.apache.juneau.swap.BeanInterceptor<?>> value) {
+			super.beanInterceptor(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanMapPutReturnsOldValue() {
+			super.beanMapPutReturnsOldValue();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanMethodVisibility(Visibility value) {
+			super.beanMethodVisibility(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanProperties(Class<?> beanClass, String properties) {
+			super.beanProperties(beanClass, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanProperties(Map<String,Object> values) {
+			super.beanProperties(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanProperties(String beanClassName, String properties) {
+			super.beanProperties(beanClassName, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesExcludes(Class<?> beanClass, String properties) {
+			super.beanPropertiesExcludes(beanClass, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesExcludes(Map<String,Object> values) {
+			super.beanPropertiesExcludes(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesExcludes(String beanClassName, String properties) {
+			super.beanPropertiesExcludes(beanClassName, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesReadOnly(Class<?> beanClass, String properties) {
+			super.beanPropertiesReadOnly(beanClass, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesReadOnly(Map<String,Object> values) {
+			super.beanPropertiesReadOnly(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesReadOnly(String beanClassName, String properties) {
+			super.beanPropertiesReadOnly(beanClassName, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesWriteOnly(Class<?> beanClass, String properties) {
+			super.beanPropertiesWriteOnly(beanClass, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesWriteOnly(Map<String,Object> values) {
+			super.beanPropertiesWriteOnly(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beanPropertiesWriteOnly(String beanClassName, String properties) {
+			super.beanPropertiesWriteOnly(beanClassName, properties);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beansRequireDefaultConstructor() {
+			super.beansRequireDefaultConstructor();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beansRequireSerializable() {
+			super.beansRequireSerializable();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder beansRequireSettersForGetters() {
+			super.beansRequireSettersForGetters();
+			return this;
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public YamlSerializer build() {
+			return cache(CACHE).build(YamlSerializer.class);
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) {
+			super.cache(value);
+			return this;
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public Builder copy() {
+			return new Builder(this);
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder debug() {
+			super.debug();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder debug(boolean value) {
+			super.debug(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder detectRecursions() {
+			super.detectRecursions();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder detectRecursions(boolean value) {
+			super.detectRecursions(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder dictionaryOn(Class<?> on, java.lang.Class<?>...values) {
+			super.dictionaryOn(on, values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder disableBeansRequireSomeProperties() {
+			super.disableBeansRequireSomeProperties();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder disableIgnoreMissingSetters() {
+			super.disableIgnoreMissingSetters();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder disableIgnoreTransientFields() {
+			super.disableIgnoreTransientFields();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder disableIgnoreUnknownNullBeanProperties() {
+			super.disableIgnoreUnknownNullBeanProperties();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder disableInterfaceProxies() {
+			super.disableInterfaceProxies();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public <T> Builder example(Class<T> pojoClass, String json) {
+			super.example(pojoClass, json);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public <T> Builder example(Class<T> pojoClass, T o) {
+			super.example(pojoClass, o);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder fileCharset(Charset value) {
+			super.fileCharset(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder findFluentSetters() {
+			super.findFluentSetters();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder findFluentSetters(Class<?> on) {
+			super.findFluentSetters(on);
+			return this;
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public HashKey hashKey() {
+			return HashKey.of(
+				super.hashKey(),
+				addBeanTypesYaml
+			);
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ignoreInvocationExceptionsOnGetters() {
+			super.ignoreInvocationExceptionsOnGetters();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ignoreInvocationExceptionsOnSetters() {
+			super.ignoreInvocationExceptionsOnSetters();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ignoreRecursions() {
+			super.ignoreRecursions();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ignoreRecursions(boolean value) {
+			super.ignoreRecursions(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ignoreUnknownBeanProperties() {
+			super.ignoreUnknownBeanProperties();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ignoreUnknownEnumValues() {
+			super.ignoreUnknownEnumValues();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder impl(Context value) {
+			super.impl(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder implClass(Class<?> interfaceClass, Class<?> implClass) {
+			super.implClass(interfaceClass, implClass);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder implClasses(Map<Class<?>,Class<?>> values) {
+			super.implClasses(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder initialDepth(int value) {
+			super.initialDepth(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder interfaceClass(Class<?> on, Class<?> value) {
+			super.interfaceClass(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder interfaces(java.lang.Class<?>...value) {
+			super.interfaces(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder keepNullProperties() {
+			super.keepNullProperties();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder keepNullProperties(boolean value) {
+			super.keepNullProperties(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder listener(Class<? extends org.apache.juneau.serializer.SerializerListener> value) {
+			super.listener(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder locale(Locale value) {
+			super.locale(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder maxDepth(int value) {
+			super.maxDepth(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder maxIndent(int value) {
+			super.maxIndent(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder mediaType(MediaType value) {
+			super.mediaType(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder notBeanClasses(java.lang.Class<?>...values) {
+			super.notBeanClasses(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder notBeanPackages(String...values) {
+			super.notBeanPackages(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder produces(String value) {
+			super.produces(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder propertyNamer(Class<?> on, Class<? extends org.apache.juneau.PropertyNamer> value) {
+			super.propertyNamer(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder propertyNamer(Class<? extends org.apache.juneau.PropertyNamer> value) {
+			super.propertyNamer(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder quoteChar(char value) {
+			super.quoteChar(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder quoteCharOverride(char value) {
+			super.quoteCharOverride(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder sortCollections() {
+			super.sortCollections();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder sortCollections(boolean value) {
+			super.sortCollections(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder sortMaps() {
+			super.sortMaps();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder sortMaps(boolean value) {
+			super.sortMaps(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder sortProperties() {
+			super.sortProperties();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder sortProperties(java.lang.Class<?>...on) {
+			super.sortProperties(on);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder sq() {
+			super.sq();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder stopClass(Class<?> on, Class<?> value) {
+			super.stopClass(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder streamCharset(Charset value) {
+			super.streamCharset(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public <T,S> Builder swap(Class<T> normalClass, Class<S> swappedClass, ThrowingFunction<T,S> swapFunction) {
+			super.swap(normalClass, swappedClass, swapFunction);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public <T,S> Builder swap(Class<T> normalClass, Class<S> swappedClass, ThrowingFunction<T,S> swapFunction, ThrowingFunction<S,T> unswapFunction) {
+			super.swap(normalClass, swappedClass, swapFunction, unswapFunction);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder swaps(Class<?>...values) {
+			super.swaps(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder swaps(Object...values) {
+			super.swaps(values);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder timeZone(TimeZone value) {
+			super.timeZone(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder trimEmptyCollections() {
+			super.trimEmptyCollections();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder trimEmptyCollections(boolean value) {
+			super.trimEmptyCollections(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder trimEmptyMaps() {
+			super.trimEmptyMaps();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder trimEmptyMaps(boolean value) {
+			super.trimEmptyMaps(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder trimStrings() {
+			super.trimStrings();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder trimStrings(boolean value) {
+			super.trimStrings(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder type(Class<? extends org.apache.juneau.Context> value) {
+			super.type(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder typeName(Class<?> on, String value) {
+			super.typeName(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder typePropertyName(Class<?> on, String value) {
+			super.typePropertyName(on, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder typePropertyName(String value) {
+			super.typePropertyName(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder uriContext(UriContext value) {
+			super.uriContext(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder uriRelativity(UriRelativity value) {
+			super.uriRelativity(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder uriResolution(UriResolution value) {
+			super.uriResolution(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder useEnumNames() {
+			super.useEnumNames();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder useJavaBeanIntrospector() {
+			super.useJavaBeanIntrospector();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder useWhitespace() {
+			super.useWhitespace();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder useWhitespace(boolean value) {
+			super.useWhitespace(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder ws() {
+			super.ws();
+			return this;
+		}
+	}
+
+	/** Default serializer, with whitespace. */
+	public static class Readable extends YamlSerializer {
+
+		/**
+		 * Constructor.
+		 *
+		 * @param builder The builder for this object.
+		 */
+		public Readable(Builder builder) {
+			super(builder.useWhitespace());
+		}
+	}
+
+	/** Default serializer, all default settings. */
+	public static final YamlSerializer DEFAULT = new YamlSerializer(create());
+
+	/** Default serializer, with whitespace. */
+	public static final YamlSerializer DEFAULT_READABLE = new Readable(create());
+
+	/**
+	 * Creates a new builder for this object.
+	 *
+	 * @return A new builder.
+	 */
+	public static Builder create() {
+		return new Builder();
+	}
+
+	protected final boolean addBeanTypesYaml;
+
+	private final boolean addBeanTypes2;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param builder The builder for this object.
+	 */
+	public YamlSerializer(Builder builder) {
+		super(builder);
+		addBeanTypesYaml = builder.addBeanTypesYaml;
+		addBeanTypes2 = addBeanTypesYaml || super.isAddBeanTypes();
+	}
+
+	@Override /* Overridden from Context */
+	public Builder copy() {
+		return new Builder(this);
+	}
+
+	@Override /* Overridden from Context */
+	public YamlSerializerSession.Builder createSession() {
+		return YamlSerializerSession.create(this);
+	}
+
+	@Override /* Overridden from Context */
+	public YamlSerializerSession getSession() { return createSession().build(); }
+
+	/**
+	 * Add <js>"_type"</js> properties when needed.
+	 *
+	 * @see Builder#addBeanTypesYaml()
+	 * @return
+	 * 	<jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred
+	 * 	through reflection.
+	 */
+	@Override
+	protected final boolean isAddBeanTypes() { return addBeanTypes2; }
+
+	@Override /* Overridden from WriterSerializer */
+	protected FluentMap<String,Object> properties() {
+		return super.properties()
+			.a(PROP_addBeanTypesYaml, addBeanTypesYaml);
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializerSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializerSession.java
new file mode 100644
index 0000000000..bd9ea06ae7
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializerSession.java
@@ -0,0 +1,453 @@
+/*
+ * 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.yaml;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.IoUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.function.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.lang.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link YamlSerializer}.
+ *
+ * <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/YamlBasics">YAML Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+	"resource", // Resource management handled externally
+	"java:S110", // Inheritance depth acceptable for this class hierarchy
+	"java:S115" // Constants use UPPER_snakeCase naming convention
+})
+public class YamlSerializerSession extends WriterSerializerSession {
+
+	// Argument name constants for assertArgNotNull
+	private static final String ARG_ctx = "ctx";
+
+	/**
+	 * Builder class.
+	 */
+	public static class Builder extends WriterSerializerSession.Builder {
+
+		private YamlSerializer ctx;
+
+		/**
+		 * Constructor
+		 *
+		 * @param ctx The context creating this session.
+		 * 	<br>Cannot be <jk>null</jk>.
+		 */
+		protected Builder(YamlSerializer ctx) {
+			super(assertArgNotNull(ARG_ctx, ctx));
+			this.ctx = ctx;
+		}
+
+		@Override /* Overridden from Builder */
+		public <T> Builder apply(Class<T> type, Consumer<T> apply) {
+			super.apply(type, apply);
+			return this;
+		}
+
+		@Override
+		public YamlSerializerSession build() {
+			return new YamlSerializerSession(this);
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder debug(Boolean value) {
+			super.debug(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder fileCharset(Charset value) {
+			super.fileCharset(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder javaMethod(Method value) {
+			super.javaMethod(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder locale(Locale value) {
+			super.locale(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder mediaType(MediaType value) {
+			super.mediaType(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder mediaTypeDefault(MediaType value) {
+			super.mediaTypeDefault(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder properties(Map<String,Object> value) {
+			super.properties(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder property(String key, Object value) {
+			super.property(key, value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder resolver(VarResolverSession value) {
+			super.resolver(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder schema(HttpPartSchema value) {
+			super.schema(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder schemaDefault(HttpPartSchema value) {
+			super.schemaDefault(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder streamCharset(Charset value) {
+			super.streamCharset(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder timeZone(TimeZone value) {
+			super.timeZone(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder timeZoneDefault(TimeZone value) {
+			super.timeZoneDefault(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder unmodifiable() {
+			super.unmodifiable();
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder uriContext(UriContext value) {
+			super.uriContext(value);
+			return this;
+		}
+
+		@Override /* Overridden from Builder */
+		public Builder useWhitespace(Boolean value) {
+			super.useWhitespace(value);
+			return 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(YamlSerializer ctx) {
+		return new Builder(assertArgNotNull(ARG_ctx, ctx));
+	}
+
+	private final YamlSerializer ctx;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param builder The builder for this object.
+	 */
+	protected YamlSerializerSession(Builder builder) {
+		super(builder);
+		this.ctx = builder.ctx;
+	}
+
+	@Override /* Overridden from SerializerSession */
+	protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
+		serializeAnything(getYamlWriter(out), o, getExpectedRootType(o), "root", null, true);
+	}
+
+	/**
+	 * Converts the specified output target object to a {@link YamlWriter}.
+	 *
+	 * @param out The output target object.
+	 * @return The output target object wrapped in a {@link YamlWriter}.
+	 */
+	protected final YamlWriter getYamlWriter(SerializerPipe out) {
+		var output = out.getRawOutput();
+		if (output instanceof YamlWriter output2)
+			return output2;
+		var w = new YamlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getUriResolver());
+		out.setWriter(w);
+		return w;
+	}
+
+	/**
+	 * Workhorse method.
+	 * Determines the type of object, and then calls the appropriate type-specific serialization method.
+	 *
+	 * @param out The output writer.
+	 * @param o The object to serialize.
+	 * @param eType The expected type.
+	 * @param attrName The attribute name.
+	 * @param pMeta The bean property currently being parsed.
+	 * @param asField If <jk>true</jk>, this value appears inline after a key or at root level.
+	 * 	If <jk>false</jk>, this value needs its own indentation as a standalone block.
+	 * @return The same writer passed in.
+	 * @throws SerializeException General serialization error occurred.
+	 */
+	@SuppressWarnings({
+		"rawtypes", // Raw types necessary for generic type handling
+		"java:S3776", // Cognitive complexity acceptable for this specific logic
+	})
+	protected YamlWriter serializeAnything(YamlWriter out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta, boolean asField) throws SerializeException {
+
+		if (o == null) {
+			out.append("null");
+			return out;
+		}
+
+		if (eType == null)
+			eType = object();
+
+		ClassMeta<?> aType;
+		ClassMeta<?> sType;
+
+		aType = push2(attrName, o, eType);
+		boolean isRecursion = aType == null;
+
+		if (aType == null) {
+			o = null;
+			aType = object();
+		}
+
+		if (isOptional(aType)) {
+			o = getOptionalValue(o);
+			eType = getOptionalType(eType);
+			aType = getClassMetaForObject(o, object());
+		}
+
+		sType = aType;
+		String typeName = getBeanTypeName(this, eType, aType, pMeta);
+
+		var swap = aType.getSwap(this);
+		if (nn(swap)) {
+			o = swap(swap, o);
+			sType = swap.getSwapClassMeta(this);
+
+			if (sType.isObject())
+				sType = getClassMetaForObject(o);
+		}
+
+		if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) {
+			out.append("null");
+		} else if (sType.isNumber() || sType.isBoolean()) {
+			out.append(o);
+		} else if (sType.isBean()) {
+			serializeBeanMap(out, toBeanMap(o), typeName);
+		} else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
+			out.uriValue(o);
+		} else if (sType.isMap()) {
+			if (o instanceof BeanMap o2)
+				serializeBeanMap(out, o2, typeName);
+			else
+				serializeMap(out, (Map)o, eType);
+		} else if (sType.isCollection()) {
+			serializeCollection(out, (Collection)o, eType, asField);
+		} else if (sType.isArray()) {
+			serializeCollection(out, toList(sType.inner(), o), eType, asField);
+		} else if (sType.isReader()) {
+			pipe((Reader)o, out, SerializerSession::handleThrown);
+		} else if (sType.isInputStream()) {
+			pipe((InputStream)o, out, SerializerSession::handleThrown);
+		} else {
+			out.textValue(toString(o));
+		}
+
+		if (! isRecursion)
+			pop();
+		return out;
+	}
+
+	/**
+	 * Determines whether a ClassMeta represents a complex type that requires block-style YAML formatting.
+	 */
+	private boolean isComplexValue(ClassMeta<?> cm) {
+		if (cm == null)
+			return false;
+		if (cm.isMapOrBean() || cm.isCollectionOrArrayOrOptional())
+			return true;
+		var swap = cm.getSwap(this);
+		if (swap != null) {
+			var swapType = swap.getSwapClassMeta(this);
+			return swapType.isMapOrBean() || swapType.isCollectionOrArrayOrOptional();
+		}
+		return false;
+	}
+
+	private SerializerWriter serializeBeanMap(YamlWriter out, BeanMap<?> m, String typeName) throws SerializeException {
+		int i = indent - 1;
+
+		var addAtt = Flag.create();
+		Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
+
+		if (nn(typeName)) {
+			BeanPropertyMeta pm = m.getMeta().getTypeProperty();
+			out.key(pm.getName()).textValue(typeName);
+			addAtt.set();
+		}
+
+		m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
+			var cMeta = pMeta.getClassMeta();
+			if (nn(thrown))
+				onBeanGetterException(pMeta, thrown);
+
+			if (canIgnoreValue(cMeta, key, value))
+				return;
+
+			addAtt.ifSet(() -> out.yamlIndent(i)).set();
+
+			ClassMeta<?> actualType = value == null ? cMeta : getClassMetaForObject(value, cMeta);
+			if (value != null && isComplexValue(actualType)) {
+				out.keyName(key).w(": ");
+				if (actualType.isCollectionOrArrayOrOptional()) {
+					serializeAnything(out, value, cMeta, key, pMeta, false);
+				} else {
+					out.yamlIndent(i + 1);
+					serializeAnything(out, value, cMeta, key, pMeta, true);
+				}
+			} else {
+				out.key(key);
+				serializeAnything(out, value, cMeta, key, pMeta, true);
+			}
+		});
+
+		if (addAtt.isUnset())
+			out.w("{}");
+
+		return out;
+	}
+
+	@SuppressWarnings({
+		"rawtypes", // Raw types necessary for generic collection/map serialization
+		"unchecked", // Type erasure requires unchecked casts in collection/map serialization
+	})
+	private SerializerWriter serializeMap(YamlWriter out, Map m, ClassMeta<?> type) throws SerializeException {
+		int i = indent - 1;
+
+		var keyType = type.getKeyType();
+		var valueType = type.getValueType();
+
+		if (m.isEmpty()) {
+			out.w("{}");
+			return out;
+		}
+
+		var addAtt = Flag.create();
+		forEachEntry(m, x -> {
+			addAtt.ifSet(() -> out.yamlIndent(i)).set();
+			Object value = x.getValue();
+			Object key = generalize(x.getKey(), keyType);
+
+			ClassMeta<?> actualType = value == null ? valueType : getClassMetaForObject(value, valueType);
+			if (value != null && isComplexValue(actualType)) {
+				out.keyName(toString(key)).w(": ");
+				if (actualType.isCollectionOrArrayOrOptional()) {
+					serializeAnything(out, value, valueType, (key == null ? null : toString(key)), null, false);
+				} else {
+					out.yamlIndent(i + 1);
+					serializeAnything(out, value, valueType, (key == null ? null : toString(key)), null, true);
+				}
+			} else {
+				out.key(toString(key));
+				serializeAnything(out, value, valueType, (key == null ? null : toString(key)), null, true);
+			}
+		});
+
+		return out;
+	}
+
+	@SuppressWarnings({
+		"rawtypes", // Raw types necessary for generic collection/map serialization
+		"unchecked", // Type erasure requires unchecked casts in collection/map serialization
+	})
+	private SerializerWriter serializeCollection(YamlWriter out, Collection c, ClassMeta<?> type, boolean asField) throws SerializeException {
+		int i = indent - 1;
+
+		if (c.isEmpty()) {
+			out.w("[]");
+			return out;
+		}
+
+		var elementType = type.getElementType();
+
+		var addEntry = Flag.create();
+		forEachEntry(c, x -> {
+			if (asField && addEntry.isUnset()) {
+				out.w("- ");
+			} else {
+				out.listEntry(i);
+			}
+			addEntry.set();
+			serializeAnything(out, x, elementType, "<iterator>", null, true);
+		});
+
+		return out;
+	}
+
+	/**
+	 * Add <js>"_type"</js> properties when needed.
+	 *
+	 * @return
+	 * 	<jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred
+	 * 	through reflection.
+	 */
+	@Override
+	protected final boolean isAddBeanTypes() { return ctx.isAddBeanTypes(); }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlWriter.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlWriter.java
new file mode 100644
index 0000000000..b2e94a11c7
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlWriter.java
@@ -0,0 +1,409 @@
+/*
+ * 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.yaml;
+
+import static org.apache.juneau.commons.utils.StringUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.lang.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Specialized writer for serializing YAML.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * 	<li class='note'>
+ * 		This class is not intended for external use.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/YamlBasics">YAML Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+	"resource" // Writer resource managed by calling code
+})
+public class YamlWriter extends SerializerWriter {
+
+	private static final AsciiSet specialChars = AsciiSet.of(":#{}[],&*!|>'\"%@`");
+
+	// @formatter:off
+	private static final Set<String> reservedWords = new HashSet<>(Arrays.asList(
+		"null", "Null", "NULL", "~",
+		"true", "True", "TRUE",
+		"false", "False", "FALSE",
+		"yes", "Yes", "YES",
+		"no", "No", "NO",
+		"on", "On", "ON",
+		"off", "Off", "OFF",
+		".inf", ".Inf", ".INF",
+		"-.inf", "-.Inf", "-.INF",
+		".nan", ".NaN", ".NAN"
+	));
+	// @formatter:on
+
+	private static final AsciiSet escapedChars = AsciiSet.of("\\\"\n\t\r\0\u0007\b\f\u001b");
+
+	/**
+	 * Constructor.
+	 *
+	 * @param out The writer being wrapped.
+	 * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output.
+	 * @param maxIndent The maximum indentation level.
+	 * @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized.
+	 * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form.
+	 */
+	protected YamlWriter(Writer out, boolean useWhitespace, int maxIndent, boolean trimStrings, UriResolver uriResolver) {
+		super(out, useWhitespace, maxIndent, trimStrings, '"', uriResolver);
+	}
+
+	/**
+	 * Writes a YAML mapping key.
+	 *
+	 * <p>
+	 * If the key needs quoting (contains special YAML characters, starts/ends with whitespace,
+	 * is empty, or is a YAML reserved word), it will be double-quoted with proper escaping.
+	 * A colon-space separator is always appended after the key.
+	 *
+	 * @param s The key name.
+	 * @return This object.
+	 */
+	public YamlWriter key(String s) {
+		keyName(s);
+		w(": ");
+		return this;
+	}
+
+	/**
+	 * Writes just the key name portion (with quoting if necessary) but without any separator.
+	 *
+	 * @param s The key name.
+	 * @return This object.
+	 */
+	public YamlWriter keyName(String s) {
+		if (trimStrings)
+			s = trim(s);
+
+		if (s == null) {
+			w("null");
+		} else if (needsQuoting(s)) {
+			writeQuoted(s);
+		} else {
+			w(s);
+		}
+		return this;
+	}
+
+	/**
+	 * Writes a YAML scalar text value.
+	 *
+	 * <p>
+	 * If null, writes the literal <c>null</c>. If the string needs quoting (is empty,
+	 * looks like a boolean/null/number, contains special characters, starts/ends with whitespace,
+	 * or contains newlines), it will be double-quoted with proper YAML escaping.
+	 *
+	 * @param s The string value.
+	 * @return This object.
+	 */
+	public YamlWriter textValue(String s) {
+		if (trimStrings)
+			s = trim(s);
+
+		if (s == null) {
+			w("null");
+		} else if (needsQuoting(s)) {
+			writeQuoted(s);
+		} else {
+			w(s);
+		}
+		return this;
+	}
+
+	/**
+	 * Appends a URI value to the output.
+	 *
+	 * <p>
+	 * Resolves the URI and writes it as a double-quoted YAML string.
+	 *
+	 * @param uri The URI to append to the output.
+	 * @return This object.
+	 */
+	public YamlWriter uriValue(Object uri) {
+		writeQuoted(uriResolver.resolve(uri));
+		return this;
+	}
+
+	/**
+	 * Writes YAML block indentation.
+	 *
+	 * <p>
+	 * Writes a newline followed by {@code depth * 2} spaces of indentation.
+	 *
+	 * @param depth The indentation depth.
+	 * @return This object.
+	 */
+	public YamlWriter yamlIndent(int depth) {
+		w('\n');
+		for (var i = 0; i < depth * 2; i++)
+			w(' ');
+		return this;
+	}
+
+	/**
+	 * Writes a YAML list entry prefix.
+	 *
+	 * <p>
+	 * Writes a newline followed by {@code depth * 2} spaces and a dash-space (<js>"- "</js>) prefix.
+	 *
+	 * @param depth The indentation depth.
+	 * @return This object.
+	 */
+	public YamlWriter listEntry(int depth) {
+		w('\n');
+		for (var i = 0; i < depth * 2; i++)
+			w(' ');
+		w("- ");
+		return this;
+	}
+
+	/**
+	 * Returns <jk>true</jk> if the specified string needs double-quoting in YAML.
+	 *
+	 * @param s The string to check.
+	 * @return <jk>true</jk> if the string needs quoting.
+	 */
+	@SuppressWarnings({
+		"java:S3776" // Cognitive complexity acceptable for YAML quoting checks
+	})
+	private boolean needsQuoting(String s) {
+		if (s.isEmpty())
+			return true;
+
+		if (Character.isWhitespace(s.charAt(0)) || Character.isWhitespace(s.charAt(s.length() - 1)))
+			return true;
+
+		if (reservedWords.contains(s))
+			return true;
+
+		if (looksLikeNumber(s))
+			return true;
+
+		for (var i = 0; i < s.length(); i++) {
+			var c = s.charAt(i);
+			if (c == '\n' || c == '\r')
+				return true;
+			if (specialChars.contains(c))
+				return true;
+			if (escapedChars.contains(c))
+				return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Returns <jk>true</jk> if the string looks like a YAML number (integer, float, hex, or octal).
+	 */
+	private static boolean looksLikeNumber(String s) {
+		if (s.isEmpty())
+			return false;
+		var c0 = s.charAt(0);
+		if (c0 == '+' || c0 == '-')
+			return s.length() > 1 && isDigitOrDot(s.charAt(1));
+		if (c0 == '.')
+			return s.length() > 1 && Character.isDigit(s.charAt(1));
+		if (c0 == '0' && s.length() > 1) {
+			var c1 = s.charAt(1);
+			if (c1 == 'x' || c1 == 'X' || c1 == 'o' || c1 == 'O')
+				return true;
+		}
+		return Character.isDigit(c0);
+	}
+
+	private static boolean isDigitOrDot(char c) {
+		return Character.isDigit(c) || c == '.';
+	}
+
+	/**
+	 * Writes a double-quoted YAML string with proper escaping.
+	 */
+	private void writeQuoted(String s) {
+		w('"');
+		if (s != null) {
+			for (var i = 0; i < s.length(); i++) {
+				var c = s.charAt(i);
+				if (escapedChars.contains(c)) {
+					switch (c) {
+						case '\\': w('\\').w('\\'); break;
+						case '"':  w('\\').w('"'); break;
+						case '\n': w('\\').w('n'); break;
+						case '\t': w('\\').w('t'); break;
+						case '\r': w('\\').w('r'); break;
+						case '\0': w('\\').w('0'); break;
+						case '\u0007': w('\\').w('a'); break;
+						case '\b': w('\\').w('b'); break;
+						case '\f': w('\\').w('f'); break;
+						case '\u001b': w('\\').w('e'); break;
+						default: w(c);
+					}
+				} else {
+					w(c);
+				}
+			}
+		}
+		w('"');
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Overridden methods - return type covariance for chaining
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter append(char c) {
+		super.append(c);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter append(char[] value) {
+		super.append(value);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter append(int indent, char c) {
+		super.append(indent, c);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter append(int indent, String text) {
+		super.append(indent, text);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter append(Object text) {
+		super.append(text);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter append(String text) {
+		super.append(text);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter appendIf(boolean b, char c) {
+		super.appendIf(b, c);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter appendIf(boolean b, String text) {
+		super.appendIf(b, text);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter appendln(int indent, String text) {
+		super.appendln(indent, text);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter appendln(String text) {
+		super.appendln(text);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter appendUri(Object value) {
+		super.appendUri(value);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter cr(int depth) {
+		super.cr(depth);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter cre(int depth) {
+		super.cre(depth);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter i(int indent) {
+		super.i(indent);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter ie(int indent) {
+		super.ie(indent);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter nl(int indent) {
+		super.nl(indent);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter nlIf(boolean flag, int indent) {
+		super.nlIf(flag, indent);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter q() {
+		super.q();
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter s() {
+		super.s();
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter sIf(boolean flag) {
+		super.sIf(flag);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter w(char value) {
+		super.w(value);
+		return this;
+	}
+
+	@Override /* Overridden from SerializerWriter */
+	public YamlWriter w(String value) {
+		super.w(value);
+		return this;
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfig.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfig.java
new file mode 100644
index 0000000000..8ec65cadbf
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfig.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.yaml.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.yaml.*;
+
+/**
+ * Annotation for specifying config properties defined in {@link YamlSerializer} and {@link YamlParser}.
+ *
+ * <p>
+ * Used primarily for specifying bean configuration properties on REST classes and methods.
+ */
+@Target({ TYPE, METHOD })
+@Retention(RUNTIME)
+@Inherited
+@ContextApply({ YamlConfigAnnotation.SerializerApply.class, YamlConfigAnnotation.ParserApply.class })
+public @interface YamlConfig {
+
+	/**
+	 * Add <js>"_type"</js> properties when needed.
+	 *
+	 * <p>
+	 * If <js>"true"</js>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred
+	 * through reflection.
+	 *
+	 * <p>
+	 * When present, this value overrides the {@link org.apache.juneau.serializer.Serializer.Builder#addBeanTypes()} setting and is
+	 * provided to customize the behavior of specific serializers in a {@link SerializerSet}.
+	 *
+	 * <ul class='values'>
+	 * 	<li><js>"true"</js>
+	 * 	<li><js>"false"</js> (default)
+	 * </ul>
+	 *
+	 * <h5 class='section'>Notes:</h5><ul>
+	 * 	<li class='note'>
+	 * 		Supports <a class="doclink" href="https://juneau.apache.org/docs/topics/DefaultVarResolver">VarResolver.DEFAULT</a> (e.g. <js>"$C{myConfigVar}"</js>).
+	 * </ul>
+	 *
+	 * <h5 class='section'>See Also:</h5><ul>
+	 * 	<li class='jm'>{@link org.apache.juneau.yaml.YamlSerializer.Builder#addBeanTypesYaml()}
+	 * </ul>
+	 *
+	 * @return The annotation value.
+	 */
+	String addBeanTypes() default "";
+
+	/**
+	 * 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/yaml/annotation/YamlConfigAnnotation.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfigAnnotation.java
new file mode 100644
index 0000000000..f86ada422f
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfigAnnotation.java
@@ -0,0 +1,72 @@
+/*
+ * 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.yaml.annotation;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.svl.*;
+import org.apache.juneau.yaml.*;
+
+/**
+ * Utility classes and methods for the {@link YamlConfig @YamlConfig} annotation.
+ */
+public class YamlConfigAnnotation {
+
+	private YamlConfigAnnotation() {}
+
+	/**
+	 * Applies {@link YamlConfig} annotations to a {@link org.apache.juneau.yaml.YamlParser.Builder}.
+	 */
+	public static class ParserApply extends AnnotationApplier<YamlConfig,YamlParser.Builder> {
+
+		/**
+		 * Constructor.
+		 *
+		 * @param vr The resolver for resolving values in annotations.
+		 */
+		public ParserApply(VarResolverSession vr) {
+			super(YamlConfig.class, YamlParser.Builder.class, vr);
+		}
+
+		@Override
+		public void apply(AnnotationInfo<YamlConfig> ai, YamlParser.Builder b) {
+			// No parser-specific properties yet.
+		}
+	}
+
+	/**
+	 * Applies {@link YamlConfig} annotations to a {@link org.apache.juneau.yaml.YamlSerializer.Builder}.
+	 */
+	public static class SerializerApply extends AnnotationApplier<YamlConfig,YamlSerializer.Builder> {
+
+		/**
+		 * Constructor.
+		 *
+		 * @param vr The resolver for resolving values in annotations.
+		 */
+		public SerializerApply(VarResolverSession vr) {
+			super(YamlConfig.class, YamlSerializer.Builder.class, vr);
+		}
+
+		@Override
+		public void apply(AnnotationInfo<YamlConfig> ai, YamlSerializer.Builder b) {
+			YamlConfig a = ai.inner();
+
+			bool(a.addBeanTypes()).ifPresent(b::addBeanTypesYaml);
+		}
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/package-info.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/package-info.java
new file mode 100644
index 0000000000..0565eae630
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+/**
+ * YAML Annotations
+ */
+package org.apache.juneau.yaml.annotation;
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/package-info.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/package-info.java
new file mode 100644
index 0000000000..4fc497904f
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+/**
+ * YAML Marshalling Support
+ */
+package org.apache.juneau.yaml;
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 b82b79ef2d..d0677ade9d 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
@@ -28,6 +28,7 @@ import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.servlet.*;
 import org.apache.juneau.serializer.annotation.*;
 import org.apache.juneau.soap.*;
+import org.apache.juneau.yaml.*;
 import org.apache.juneau.uon.*;
 import org.apache.juneau.urlencoding.*;
 import org.apache.juneau.xml.*;
@@ -145,7 +146,8 @@ import org.apache.juneau.xml.*;
 		MsgPackSerializer.class,
 		SoapXmlSerializer.class,
 		PlainTextSerializer.class,
-		CsvSerializer.class
+		CsvSerializer.class,
+		YamlSerializer.class
 	},
 
 	// Default parsers for all Java methods in the class.
@@ -159,7 +161,8 @@ import org.apache.juneau.xml.*;
 		OpenApiParser.class,
 		MsgPackParser.class,
 		PlainTextParser.class,
-		CsvParser.class
+		CsvParser.class,
+		YamlParser.class
 	}
 )
 public interface BasicUniversalConfig extends DefaultConfig, DefaultHtmlConfig {}
\ No newline at end of file
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicYamlConfig.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicYamlConfig.java
new file mode 100644
index 0000000000..47bc301728
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicYamlConfig.java
@@ -0,0 +1,74 @@
+/*
+ * 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.rest.config;
+
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.yaml.*;
+
+/**
+ * Basic configuration for a REST resource that supports YAML transport.
+ *
+ * <p>
+ * 	Default settings defined:
+ * </p>
+ * <ul class='spaced-list'>
+ * 	<li class='ja'>{@link Rest}:
+ * 		<ul>
+ * 			<li class='jma'>{@link Rest#serializers() serializers}:
+ * 				<ul class='javatree'>
+ * 					<li class='jc'>{@link YamlSerializer}
+ * 				</ul>
+ * 			</li>
+ * 			<li class='jma'>{@link Rest#parsers() parsers}:
+ * 				<ul class='javatree'>
+ * 					<li class='jc'>{@link YamlParser}
+ * 				</ul>
+ * 			</li>
+ * 			<li class='jma'>{@link Rest#defaultAccept() defaultAccept}:  <js>"application/yaml"</js>
+ *		</ul>
+ *	</li>
+ * </ul>
+ *
+ * <p>
+ * 	This annotation can be applied to REST resource classes to define common YAML default configurations:
+ * </p>
+ * <p class='bjava'>
+ * 	<jc>// Used on a top-level resource.</jc>
+ * 	<ja>@Rest</ja>
+ * 	<jk>public class</jk> MyResource <jk>extends</jk> RestServlet <jk>implements</jk> BasicYamlConfig { ... }
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestAnnotatedClassBasics">@Rest-Annotated Class Basics</a>
+ * </ul>
+ */
+// @formatter:off
+@Rest(
+
+	// Default serializers for all Java methods in the class.
+	serializers={
+		YamlSerializer.class,
+	},
+
+	// Default parsers for all Java methods in the class.
+	parsers={
+		YamlParser.class,
+	},
+
+	defaultAccept="application/yaml"
+)
+public interface BasicYamlConfig extends DefaultConfig {}
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 f55010f7a9..385049bb9e 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
@@ -440,4 +440,70 @@ public abstract class ComboRoundTripTest_Base extends TestBase {
 	public void f24_verifyMsgPackT(ComboRoundTrip_Tester<?> t) throws Exception {
 		t.testParseVerify("msgPackT");
 	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// YAML
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@ParameterizedTest
+	@MethodSource("testers")
+	public void g11_serializeYaml(ComboRoundTrip_Tester<?> t) throws Exception {
+		t.testSerialize("yaml");
+	}
+
+	@ParameterizedTest
+	@MethodSource("testers")
+	public void g12_parseYaml(ComboRoundTrip_Tester<?> t) throws Exception {
+		t.testParse("yaml");
+	}
+
+	@ParameterizedTest
+	@MethodSource("testers")
+	public void g13_verifyYaml(ComboRoundTrip_Tester<?> t) throws Exception {
+		t.testParseVerify("yaml");
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// YAML - 't' property
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@ParameterizedTest
+	@MethodSource("testers")
+	public void g21_serializeYamlT(ComboRoundTrip_Tester<?> t) throws Exception {
+		t.testSerialize("yamlT");
+	}
+
+	@ParameterizedTest
+	@MethodSource("testers")
+	public void g22_parseYamlT(ComboRoundTrip_Tester<?> t) throws Exception {
+		t.testParse("yamlT");
+	}
+
+	@ParameterizedTest
+	@MethodSource("testers")
+	public void g23_verifyYamlT(ComboRoundTrip_Tester<?> t) throws Exception {
+		t.testParseVerify("yamlT");
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// YAML - Readable
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@ParameterizedTest
+	@MethodSource("testers")
+	public void g31_serializeYamlR(ComboRoundTrip_Tester<?> t) throws Exception {
+		t.testSerialize("yamlR");
+	}
+
+	@ParameterizedTest
+	@MethodSource("testers")
+	public void g32_parseYamlR(ComboRoundTrip_Tester<?> t) throws Exception {
+		t.testParse("yamlR");
+	}
+
+	@ParameterizedTest
+	@MethodSource("testers")
+	public void g33_verifyYamlR(ComboRoundTrip_Tester<?> t) throws Exception {
+		t.testParseVerify("yamlR");
+	}
 }
\ No newline at end of file
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 551b0fa926..b51ab17fba 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
@@ -34,6 +34,7 @@ import org.apache.juneau.serializer.*;
 import org.apache.juneau.uon.*;
 import org.apache.juneau.urlencoding.*;
 import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
 
 /**
  * Represents the input to a ComboTest.
@@ -115,6 +116,9 @@ public class ComboRoundTrip_Tester<T> {
 		public Builder<T> rdfXml(String value) { expected.put("rdfXml", value); return this; }
 		public Builder<T> rdfXmlT(String value) { expected.put("rdfXmlT", value); return this; }
 		public Builder<T> rdfXmlR(String value) { expected.put("rdfXmlR", value); return this; }
+		public Builder<T> yaml(String value) { expected.put("yaml", value); return this; }
+		public Builder<T> yamlT(String value) { expected.put("yamlT", value); return this; }
+		public Builder<T> yamlR(String value) { expected.put("yamlR", value); return this; }
 
 		public ComboRoundTrip_Tester<T> build() {
 			return new ComboRoundTrip_Tester<>(this);
@@ -160,6 +164,9 @@ public class ComboRoundTrip_Tester<T> {
 		serializers.put("urlEncR", create(b, UrlEncodingSerializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
 		serializers.put("msgPack", create(b, MsgPackSerializer.create().addBeanTypes().addRootType()));
 		serializers.put("msgPackT", create(b, MsgPackSerializer.create().typePropertyName("t").addBeanTypes().addRootType()));
+		serializers.put("yaml", create(b, YamlSerializer.DEFAULT.copy().addBeanTypes().addRootType()));
+		serializers.put("yamlT", create(b, YamlSerializer.create().typePropertyName("t").addBeanTypes().addRootType()));
+		serializers.put("yamlR", create(b, YamlSerializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
 
 		parsers.put("json", create(b, JsonParser.DEFAULT.copy()));
 		parsers.put("jsonT", create(b, JsonParser.create().typePropertyName("t")));
@@ -179,6 +186,9 @@ public class ComboRoundTrip_Tester<T> {
 		parsers.put("urlEncR", create(b, UrlEncodingParser.DEFAULT.copy()));
 		parsers.put("msgPack", create(b, MsgPackParser.DEFAULT.copy()));
 		parsers.put("msgPackT", create(b, MsgPackParser.create().typePropertyName("t")));
+		parsers.put("yaml", create(b, YamlParser.DEFAULT.copy()));
+		parsers.put("yamlT", create(b, YamlParser.create().typePropertyName("t")));
+		parsers.put("yamlR", create(b, YamlParser.DEFAULT.copy()));
 	}
 
 	private Serializer create(Builder<?> tb, Serializer.Builder sb) {
@@ -215,7 +225,7 @@ public class ComboRoundTrip_Tester<T> {
 	}
 
 	private boolean isSkipped(String testName, String expected) {
-		return "SKIP".equals(expected) || skipTest.test(testName);
+		return expected == null || "SKIP".equals(expected) || skipTest.test(testName);
 	}
 
 	public void testSerialize(String testName) throws Exception {
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 b1e6fd7c82..45ed8cce98 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
@@ -30,6 +30,7 @@ import org.apache.juneau.msgpack.*;
 import org.apache.juneau.uon.*;
 import org.apache.juneau.urlencoding.*;
 import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
 import org.junit.jupiter.params.*;
 import org.junit.jupiter.params.provider.*;
 
@@ -109,6 +110,10 @@ class RoundTripDateTime_Test extends TestBase {
 			.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
 			.returnOriginalObject()
 			.build(),
+		tester(17, "Yaml - default")
+			.serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+			.parser(YamlParser.create())
+			.build(),
 	};
 
 	static RoundTrip_Tester[] testers() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
index 13a6b2b06e..47a7142214 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.msgpack.*;
 import org.apache.juneau.uon.*;
 import org.apache.juneau.urlencoding.*;
 import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
 
 /**
  * Tests designed to serialize and parse objects to make sure we end up
@@ -101,6 +102,10 @@ public abstract class RoundTripTest_Base extends TestBase {
 			.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
 			.returnOriginalObject()
 			.build(),
+		tester(17, "Yaml - default")
+			.serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+			.parser(YamlParser.create())
+			.build(),
 	};
 
 	static RoundTrip_Tester[]  testers() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlMarshaller_Test.java b/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlMarshaller_Test.java
new file mode 100644
index 0000000000..5e8c8652cc
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlMarshaller_Test.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.yaml;
+
+import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.marshaller.*;
+import org.junit.jupiter.api.*;
+
+class YamlMarshaller_Test extends TestBase {
+
+	@Test void a01_writeAndRead() throws Exception {
+		var m = new LinkedHashMap<String,Object>();
+		m.put("a", "1");
+		m.put("b", 2);
+
+		String yaml = Yaml.of(m);
+		assertNotNull(yaml);
+
+		JsonMap parsed = Yaml.to(yaml, JsonMap.class);
+		assertEquals("1", parsed.getString("a"));
+		assertEquals(2, parsed.getInt("b"));
+	}
+
+	@Test void a02_roundTripString() throws Exception {
+		String original = "hello world";
+		String yaml = Yaml.of(original);
+		String parsed = Yaml.to(yaml, String.class);
+		assertEquals(original, parsed);
+	}
+
+	@Test void a03_roundTripNumber() throws Exception {
+		int original = 42;
+		String yaml = Yaml.of(original);
+		int parsed = Yaml.to(yaml, int.class);
+		assertEquals(original, parsed);
+	}
+
+	@Test void a04_roundTripBoolean() throws Exception {
+		String yaml = Yaml.of(true);
+		boolean parsed = Yaml.to(yaml, boolean.class);
+		assertTrue(parsed);
+	}
+
+	@Test void a05_roundTripNull() throws Exception {
+		String yaml = Yaml.of(null);
+		assertEquals("null", yaml);
+		assertNull(Yaml.to(yaml, String.class));
+	}
+
+	@Test void a06_roundTripList() throws Exception {
+		var original = List.of("a", "b", "c");
+		String yaml = Yaml.of(original);
+		JsonList parsed = Yaml.to(yaml, JsonList.class);
+		assertEquals(3, parsed.size());
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlParser_Test.java b/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlParser_Test.java
new file mode 100644
index 0000000000..883c217abf
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlParser_Test.java
@@ -0,0 +1,144 @@
+/*
+ * 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.yaml;
+
+import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+class YamlParser_Test extends TestBase {
+
+	@Test void a01_parseString() throws Exception {
+		var p = YamlParser.DEFAULT;
+		assertEquals("hello", p.parse("hello", String.class));
+	}
+
+	@Test void a02_parseQuotedString() throws Exception {
+		var p = YamlParser.DEFAULT;
+		assertEquals("hello world", p.parse("\"hello world\"", String.class));
+		assertEquals("hello world", p.parse("'hello world'", String.class));
+	}
+
+	@Test void a03_parseNumber() throws Exception {
+		var p = YamlParser.DEFAULT;
+		assertEquals(123, p.parse("123", int.class));
+		assertEquals(1.5, p.parse("1.5", double.class));
+	}
+
+	@Test void a04_parseBoolean() throws Exception {
+		var p = YamlParser.DEFAULT;
+		assertEquals(true, p.parse("true", boolean.class));
+		assertEquals(false, p.parse("false", boolean.class));
+	}
+
+	@Test void a05_parseNull() throws Exception {
+		var p = YamlParser.DEFAULT;
+		assertNull(p.parse("null", String.class));
+	}
+
+	@Test void a06_parseFlowMapping() throws Exception {
+		var p = YamlParser.DEFAULT;
+		JsonMap m = p.parse("{a: 1, b: 2}", JsonMap.class);
+		assertEquals("1", m.getString("a"));
+		assertEquals("2", m.getString("b"));
+	}
+
+	@Test void a07_parseFlowSequence() throws Exception {
+		var p = YamlParser.DEFAULT;
+		JsonList l = p.parse("[1, 2, 3]", JsonList.class);
+		assertEquals(3, l.size());
+	}
+
+	@Test void a08_parseBlockMapping() throws Exception {
+		var p = YamlParser.DEFAULT;
+		JsonMap m = p.parse("a: 1\nb: 2", JsonMap.class);
+		assertEquals("1", m.getString("a"));
+		assertEquals("2", m.getString("b"));
+	}
+
+	@Test void a09_parseBlockSequence() throws Exception {
+		var p = YamlParser.DEFAULT;
+		JsonList l = p.parse("- a\n- b\n- c", JsonList.class);
+		assertEquals(3, l.size());
+		assertEquals("a", l.getString(0));
+	}
+
+	@Test void a10_parseNestedBlockMapping() throws Exception {
+		var p = YamlParser.DEFAULT;
+		JsonMap m = p.parse("outer:\n  inner: value", JsonMap.class);
+		assertNotNull(m.get("outer"));
+	}
+
+	@Test void a11_parseComment() throws Exception {
+		var p = YamlParser.DEFAULT;
+		assertEquals("hello", p.parse("hello # this is a comment", String.class));
+	}
+
+	@Test void a12_parseEmptyInput() throws Exception {
+		var p = YamlParser.DEFAULT;
+		assertNull(p.parse("", String.class));
+	}
+
+	@Test void a13_parseTildeNull() throws Exception {
+		var p = YamlParser.DEFAULT;
+		assertNull(p.parse("~", String.class));
+	}
+
+	public static class DurationBean {
+		public java.time.Duration negative;
+		public java.time.Duration fractional;
+		public java.time.Duration large;
+	}
+
+	@Test void a14_parseBeanWithDuration() throws Exception {
+		var s = YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType().build();
+		var p = YamlParser.DEFAULT;
+
+		var x = new DurationBean();
+		x.negative = java.time.Duration.ofHours(-6);
+		x.fractional = java.time.Duration.ofSeconds(20, 345000000);
+		x.large = java.time.Duration.ofDays(365);
+
+		String yaml = s.serialize(x);
+		var x2 = p.parse(yaml, DurationBean.class);
+		assertEquals(x.negative, x2.negative);
+		assertEquals(x.fractional, x2.fractional);
+		assertEquals(x.large, x2.large);
+	}
+
+	public static class ArrayBean {
+		public boolean[][] paBoolean;
+	}
+
+	@Test void a15_parse2dArrayBean() throws Exception {
+		var s = YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType().build();
+		var p = YamlParser.DEFAULT;
+
+		var x = new ArrayBean();
+		x.paBoolean = new boolean[][]{{true, false}, {true}};
+
+		String yaml = s.serialize(x);
+		System.out.println("2D Array YAML:\n[" + yaml + "]");
+
+		var x2 = p.parse(yaml, ArrayBean.class);
+		assertEquals(true, x2.paBoolean[0][0]);
+		assertEquals(false, x2.paBoolean[0][1]);
+		assertEquals(true, x2.paBoolean[1][0]);
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/yaml/Yaml_Test.java b/juneau-utest/src/test/java/org/apache/juneau/yaml/Yaml_Test.java
new file mode 100644
index 0000000000..b818d1f6c9
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/yaml/Yaml_Test.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.yaml;
+
+import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+@SuppressWarnings({"serial"})
+class Yaml_Test extends TestBase {
+
+	@Test void a01_basicString() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		assertEquals("hello", s.serialize("hello"));
+	}
+
+	@Test void a02_basicNumber() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		assertEquals("123", s.serialize(123));
+	}
+
+	@Test void a03_basicBoolean() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		assertEquals("true", s.serialize(true));
+		assertEquals("false", s.serialize(false));
+	}
+
+	@Test void a04_basicNull() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		assertEquals("null", s.serialize(null));
+	}
+
+	@Test void a05_basicMap() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		var m = new LinkedHashMap<String,Object>();
+		m.put("a", "1");
+		m.put("b", 2);
+		// Map should serialize as: "a: '1'\nb: 2" (strings that look like numbers are quoted)
+		String result = s.serialize(m);
+		assertTrue(result.contains("a:"), "Expected 'a:' in: " + result);
+		assertTrue(result.contains("b:"), "Expected 'b:' in: " + result);
+	}
+
+	@Test void a06_basicList() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		var l = List.of("a", "b", "c");
+		String result = s.serialize(l);
+		assertTrue(result.contains("- a"), "Expected '- a' in: " + result);
+		assertTrue(result.contains("- b"), "Expected '- b' in: " + result);
+	}
+
+	@Test void a07_nestedMap() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		var inner = new LinkedHashMap<String,Object>();
+		inner.put("x", "1");
+		var outer = new LinkedHashMap<String,Object>();
+		outer.put("inner", inner);
+		String result = s.serialize(outer);
+		assertTrue(result.contains("inner:"), "Expected 'inner:' in: " + result);
+		assertTrue(result.contains("x:"), "Expected 'x:' in: " + result);
+	}
+
+	@Test void a08_mapWithList() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		var m = new LinkedHashMap<String,Object>();
+		m.put("name", "John");
+		m.put("hobbies", List.of("reading", "coding"));
+		String result = s.serialize(m);
+		assertTrue(result.contains("name: John"), "Expected 'name: John' in: " + result);
+		assertTrue(result.contains("- reading"), "Expected '- reading' in: " + result);
+	}
+
+	@Test void a09_specialCharQuoting() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		var m = new LinkedHashMap<String,Object>();
+		m.put("key", "value: with colon");
+		String result = s.serialize(m);
+		assertTrue(result.contains("\"value: with colon\"") || result.contains("'value: with colon'"),
+			"Special chars should be quoted in: " + result);
+	}
+
+	@Test void a10_reservedWords() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		var m = new LinkedHashMap<String,Object>();
+		m.put("val", "true");
+		String result = s.serialize(m);
+		assertTrue(result.contains("\"true\""), "Reserved word 'true' should be quoted in: " + result);
+	}
+
+	@Test void a11_emptyMap() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		var m = new LinkedHashMap<String,Object>();
+		String result = s.serialize(m);
+		assertNotNull(result);
+	}
+
+	@Test void a12_emptyList() throws Exception {
+		var s = YamlSerializer.DEFAULT;
+		var l = List.of();
+		String result = s.serialize(l);
+		assertNotNull(result);
+	}
+}