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 9539dd969c INI marshalling support
9539dd969c is described below

commit 9539dd969c6351165af1f35bbbe1aee04c29b6cc
Author: James Bognar <[email protected]>
AuthorDate: Wed Mar 4 11:07:40 2026 -0500

    INI marshalling support
---
 AGENTS.md                                          |   2 +-
 .../org/apache/juneau/commons/utils/Utils.java     |   9 +-
 .../src/main/java/org/apache/juneau/Context.java   |   2 +
 .../org/apache/juneau/ini/IniBeanPropertyMeta.java |  82 +++++
 .../java/org/apache/juneau/ini/IniClassMeta.java   |  53 ++++
 .../org/apache/juneau/ini/IniMetaProvider.java     |  41 +++
 .../main/java/org/apache/juneau/ini/IniParser.java | 168 +++++++++++
 .../org/apache/juneau/ini/IniParserSession.java    | 272 +++++++++++++++++
 .../java/org/apache/juneau/ini/IniSerializer.java  | 276 +++++++++++++++++
 .../apache/juneau/ini/IniSerializerSession.java    | 330 +++++++++++++++++++++
 .../main/java/org/apache/juneau/ini/IniWriter.java | 168 +++++++++++
 .../java/org/apache/juneau/ini/annotation/Ini.java |  81 +++++
 .../juneau/ini/annotation/IniAnnotation.java       |  66 +++++
 .../apache/juneau/ini/annotation/IniConfig.java    |  81 +++++
 .../juneau/ini/annotation/IniConfigAnnotation.java |  73 +++++
 .../java/org/apache/juneau/ini/package-info.java   |  39 +++
 .../java/org/apache/juneau/marshaller/Ini.java     | 180 +++++++++++
 .../org/apache/juneau/rest/client/RestClient.java  |   3 +
 .../juneau/rest/config/BasicUniversalConfig.java   |   3 +
 .../org/apache/juneau/ComboRoundTrip_Tester.java   |   4 +
 .../org/apache/juneau/ComboSerialize_Tester.java   |   3 +
 .../juneau/a/rttests/RoundTripDateTime_Test.java   |  11 +-
 .../java/org/apache/juneau/ini/IniParser_Test.java | 129 ++++++++
 .../org/apache/juneau/ini/IniRoundTrip_Test.java   | 186 ++++++++++++
 .../org/apache/juneau/ini/IniSerializer_Test.java  | 221 ++++++++++++++
 .../org/apache/juneau/marshaller/Ini_Test.java     |  69 +++++
 ...i_implementation.md => 1_ini_implementation.md} |  19 +-
 ...implementation.md => 2_hjson_implementation.md} |   0
 ...s_implementation.md => 3_jcs_implementation.md} |   0
 ..._implementation.md => 4_bson_implementation.md} |   0
 ..._implementation.md => 5_cbor_implementation.md} |   0
 ...implementation.md => 6_hocon_implementation.md} |   0
 ...plementation.md => 7_parquet_implementation.md} |   0
 33 files changed, 2546 insertions(+), 25 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 3bb7c9a80c..8b15dde388 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -470,7 +470,7 @@ timeout 120s sh -c 'mvn clean install 2>&1 | tail -20'
 - **TODO Identifiers**: When adding new TODO items, assign them a unique 
"TODO-#" identifier (e.g., "TODO-1", "TODO-2", etc.)
 - **TODO References**: When the user asks to "fix TODO-X" or "work on TODO-X", 
they are referring to the specific identifier in the TODO.md file
 - **TODO Completion**: When TODOs are completed, remove them from the TODO 
list entirely
-- **Plans**: Implementation plans (e.g., `jsonl_implementation.md`, 
`ini_implementation.md`) are also located in the `/todo` folder alongside 
TODO.md
+- **Plans**: Implementation plans (e.g., `1_ini_implementation.md`, 
`2_hjson_implementation.md`) are also located in the `/todo` folder alongside 
TODO.md
 
 ### 12. Release Notes Management
 - When the user says "add to release notes" or "add this to the release 
notes", this refers to the release notes in the `/juneau-docs` directory
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
index 6ce172a46e..1ad2eb413e 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
@@ -54,7 +54,8 @@ import org.apache.juneau.commons.settings.*;
  */
 @SuppressWarnings({
        "java:S115", // Constants use UPPER_snakeCase convention
-       "java:S1118" // Utility class with static methods only
+       "java:S1118", // Utility class with static methods only
+       "java:S1135"  // TODO comment retained as documentation for future 
refactoring
 })
 public class Utils {
 
@@ -246,7 +247,8 @@ public class Utils {
         *         Returns <c>0</c> if objects are not of the same type or do 
not implement the {@link Comparable} interface.
         */
        @SuppressWarnings({
-               "unchecked" // Type erasure requires unchecked casts
+               "unchecked", // Type erasure requires unchecked casts
+               "java:S3740" // Raw Comparable; parameterizing causes compile 
error in compareTo
        })
        public static int cmp(Object o1, Object o2) {
                if (o1 == null) {
@@ -1525,7 +1527,6 @@ public class Utils {
         * @return <jk>true</jk> if the objects are not equal.
         * @see #eq(Object, Object)
         */
-       // TODO - Rename this to neq, then add a ne for not-empty
        public static <T> boolean neq(T s1, T s2) {
                return ! eq(s1, s2);
        }
@@ -1552,7 +1553,6 @@ public class Utils {
         * @return <jk>true</jk> if the objects are not equal based on the 
test, or if one is <jk>null</jk> and the other is not.
         * @see #eq(Object, Object, BiPredicate)
         */
-       // TODO - Rename this to neq, then add a ne for not-empty
        public static <T,U> boolean neq(T o1, U o2, BiPredicate<T,U> test) {
                if (o1 == null)
                        return nn(o2);
@@ -1582,7 +1582,6 @@ public class Utils {
         * @return <jk>true</jk> if the strings are not equal ignoring case.
         * @see #eqic(String, String)
         */
-       // TODO - Rename this to neqic, then add a ne for not-empty
        public static boolean neqic(String s1, String s2) {
                return ! eqic(s1, s2);
        }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
index ce8c71ed16..4456d88810 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
@@ -32,6 +32,7 @@ import org.apache.juneau.commons.reflect.*;
 import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.csv.annotation.*;
 import org.apache.juneau.html.annotation.*;
+import org.apache.juneau.ini.annotation.*;
 import org.apache.juneau.json.annotation.*;
 import org.apache.juneau.jsonl.annotation.*;
 import org.apache.juneau.jsonschema.annotation.*;
@@ -402,6 +403,7 @@ public abstract class Context {
                 *      <li class ='ja'>{@link CsvConfig}
                 *      <li class ='ja'>{@link HtmlConfig}
                 *      <li class ='ja'>{@link HtmlDocConfig}
+                *      <li class ='ja'>{@link IniConfig}
                 *      <li class ='ja'>{@link JsonConfig}
                 *      <li class ='ja'>{@link JsonlConfig}
                 *      <li class ='ja'>{@link JsonSchemaConfig}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniBeanPropertyMeta.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniBeanPropertyMeta.java
new file mode 100644
index 0000000000..dcb97169e5
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniBeanPropertyMeta.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ini;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.ini.annotation.*;
+
+/**
+ * Metadata on bean properties specific to INI serializers and parsers, from 
{@link Ini @Ini}.
+ */
+public class IniBeanPropertyMeta extends ExtendedBeanPropertyMeta {
+
+       /** Default instance. */
+       public static final IniBeanPropertyMeta DEFAULT = new 
IniBeanPropertyMeta();
+
+       private final String section;
+       private final String comment;
+       private final boolean json5Encoding;
+
+       /**
+        * Constructor.
+        *
+        * @param bpm The bean property metadata.
+        * @param mp INI metadata provider.
+        */
+       public IniBeanPropertyMeta(BeanPropertyMeta bpm, IniMetaProvider mp) {
+               super(bpm);
+               var a = 
bpm.getAnnotations(Ini.class).map(AnnotationInfo::inner).reduce((first, second) 
-> second).orElse(null);
+               section = a != null && !a.section().isEmpty() ? a.section() : 
"";
+               comment = a != null && !a.comment().isEmpty() ? a.comment() : 
"";
+               json5Encoding = a != null && a.json5Encoding();
+       }
+
+       private IniBeanPropertyMeta() {
+               super(null);
+               section = "";
+               comment = "";
+               json5Encoding = false;
+       }
+
+       /**
+        * Returns the custom section name for this property.
+        *
+        * @return The section name, or empty string if not specified.
+        */
+       public String getSection() {
+               return section;
+       }
+
+       /**
+        * Returns the comment text to emit before this property.
+        *
+        * @return The comment, or empty string if not specified.
+        */
+       public String getComment() {
+               return comment;
+       }
+
+       /**
+        * Returns whether this property should be JSON5-encoded even when 
normally simple.
+        *
+        * @return Whether JSON5 encoding is forced.
+        */
+       public boolean isJson5Encoding() {
+               return json5Encoding;
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniClassMeta.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniClassMeta.java
new file mode 100644
index 0000000000..c230d258b7
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniClassMeta.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ini;
+
+import java.util.concurrent.atomic.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.ini.annotation.*;
+
+/**
+ * Metadata on classes specific to INI serializers and parsers, from {@link 
Ini @Ini}.
+ */
+public class IniClassMeta extends ExtendedClassMeta {
+
+       private final String section;
+
+       /**
+        * Constructor.
+        *
+        * @param cm The class metadata.
+        * @param mp INI metadata provider.
+        */
+       public IniClassMeta(ClassMeta<?> cm, IniMetaProvider mp) {
+               super(cm);
+               var ref = new AtomicReference<Ini>();
+               cm.forEachAnnotation(Ini.class, null, ref::set);
+               var a = ref.get();
+               section = a != null && !a.section().isEmpty() ? a.section() : 
"";
+       }
+
+       /**
+        * Returns the custom section name for this class.
+        *
+        * @return The section name, or empty string if not specified.
+        */
+       public String getSection() {
+               return section;
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniMetaProvider.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniMetaProvider.java
new file mode 100644
index 0000000000..e48f2ab233
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniMetaProvider.java
@@ -0,0 +1,41 @@
+/*
+ * 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.ini;
+
+import org.apache.juneau.*;
+
+/**
+ * Interface for providing access to {@link IniClassMeta} and {@link 
IniBeanPropertyMeta} objects.
+ */
+public interface IniMetaProvider {
+
+       /**
+        * Returns the INI-specific metadata on the specified bean property.
+        *
+        * @param bpm The bean property to return the metadata on.
+        * @return The metadata.
+        */
+       IniBeanPropertyMeta getIniBeanPropertyMeta(BeanPropertyMeta bpm);
+
+       /**
+        * Returns the INI-specific metadata on the specified class.
+        *
+        * @param cm The class to return the metadata on.
+        * @return The metadata.
+        */
+       IniClassMeta getIniClassMeta(ClassMeta<?> cm);
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniParser.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniParser.java
new file mode 100644
index 0000000000..41f7078b08
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniParser.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ini;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Parses INI-formatted text into POJO models.
+ *
+ * <p>
+ * Parses INI files (section/key-value format) into Java beans and maps.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Consumes: <bc>text/ini, text/x-ini</bc>
+ *
+ * <h5 class='topic'>Value parsing</h5>
+ * <ul class='spaced-list'>
+ *     <li>Unquoted <c>null</c> → Java <jk>null</jk>
+ *     <li>Unquoted <c>true</c>/<c>false</c> → Boolean
+ *     <li>Unquoted numbers → Number (int, long, float, double as appropriate)
+ *     <li>Single-quoted strings (<c>'...'</c>) → String (with <c>''</c> 
unescaped to single quote)
+ *     <li>Values starting with <c>[</c> or <c>{</c> → Delegated to JSON parser
+ *     <li>ISO 8601 strings → Date, Calendar, or <c>java.time.*</c> when 
target requires it
+ *     <li>ISO 8601 duration strings → {@link java.time.Duration}
+ *     <li>Other unquoted tokens → String
+ * </ul>
+ *
+ * <h5 class='topic'>Sections</h5>
+ * <p>
+ * Sections (e.g. <c>[address]</c>) map to nested bean properties or Map 
properties.
+ * Section paths like <c>[employment/company]</c> map to deeply nested bean 
properties.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     <jc>// Parse INI into a bean</jc>
+ *     MyConfig <jv>config</jv> = 
IniParser.<jsf>DEFAULT</jsf>.parse(<jv>ini</jv>, MyConfig.<jk>class</jk>);
+ *
+ *     <jc>// Parse into a Map</jc>
+ *     Map&lt;String, Object&gt; <jv>map</jv> = 
IniParser.<jsf>DEFAULT</jsf>.parse(<jv>ini</jv>, Map.<jk>class</jk>, 
String.<jk>class</jk>, Object.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='figure'>Example input:</h5>
+ * <p class='bini'>
+ *     <ck>name</ck> = <cv>Alice</cv>
+ *     <ck>age</ck> = <cv>30</cv>
+ *
+ *     <cs>[address]</cs>
+ *     <ck>street</ck> = <cv>123 Main St</cv>
+ *     <ck>city</ck> = <cv>Boston</cv>
+ *     <ck>state</ck> = <cv>MA</cv>
+ * </p>
+ *
+ * <h5 class='topic'>Limitations</h5>
+ * <p>
+ * Parsing into top-level collections, arrays, or scalar types is not 
supported.
+ * The target type must be a bean class or <c>Map&lt;String,?&gt;</c>.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ *     <li class='note'>This class is thread safe and reusable.
+ *     <li class='note'>Values starting with <c>[</c> or <c>{</c> are 
delegated to the JSON parser.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/IniBasics";>INI Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+       "java:S110", "java:S115"
+})
+public class IniParser extends ReaderParser implements IniMetaProvider {
+
+       private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>, 
IniClassMeta> iniClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
+       private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta, 
IniBeanPropertyMeta> iniBeanPropertyMetas = new 
java.util.concurrent.ConcurrentHashMap<>();
+
+       private static final String ARG_copyFrom = "copyFrom";
+
+       /**
+        * Builder for {@link IniParser}.
+        */
+       public static class Builder extends ReaderParser.Builder {
+
+               private static final Cache<HashKey,IniParser> CACHE = 
Cache.of(HashKey.class, IniParser.class).build();
+
+               protected Builder() {
+                       consumes("text/ini,text/x-ini");
+               }
+
+               protected Builder(Builder copyFrom) {
+                       super(assertArgNotNull(ARG_copyFrom, copyFrom));
+               }
+
+               protected Builder(IniParser copyFrom) {
+                       super(assertArgNotNull(ARG_copyFrom, copyFrom));
+               }
+
+               @Override
+               public IniParser build() {
+                       return cache(CACHE).build(IniParser.class);
+               }
+
+               @Override
+               public Builder copy() {
+                       return new Builder(this);
+               }
+       }
+
+       /** Default parser instance. */
+       public static final IniParser DEFAULT = new IniParser(create());
+
+       /**
+        * Creates a new builder.
+        *
+        * @return A new builder.
+        */
+       public static Builder create() {
+               return new Builder();
+       }
+
+       /**
+        * Constructor.
+        *
+        * @param builder The builder.
+        */
+       public IniParser(Builder builder) {
+               super(builder);
+       }
+
+       @Override
+       public IniBeanPropertyMeta getIniBeanPropertyMeta(BeanPropertyMeta bpm) 
{
+               if (bpm == null)
+                       return IniBeanPropertyMeta.DEFAULT;
+               return iniBeanPropertyMetas.computeIfAbsent(bpm, k -> new 
IniBeanPropertyMeta(k, this));
+       }
+
+       @Override
+       public IniClassMeta getIniClassMeta(ClassMeta<?> cm) {
+               return iniClassMetas.computeIfAbsent(cm, k -> new 
IniClassMeta(k, this));
+       }
+
+       @Override
+       public IniParserSession.Builder createSession() {
+               return IniParserSession.create(this);
+       }
+
+       @Override
+       public Builder copy() {
+               return new Builder(this);
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniParserSession.java
new file mode 100644
index 0000000000..ff61d45613
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniParserSession.java
@@ -0,0 +1,272 @@
+/*
+ * 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.ini;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.Map.*;
+import java.util.regex.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.utils.Iso8601Utils;
+
+/**
+ * Session for parsing INI format into POJOs.
+ */
+@SuppressWarnings({
+       "unchecked",
+       "java:S115", // ARG_ctx follows project assertion-param naming 
convention (ARG_<param>)
+       "java:S3776", "java:S6541", "java:S135"
+})
+public class IniParserSession extends ReaderParserSession {
+
+       private static final String ARG_ctx = "ctx";
+
+       /** Pattern for key=value or key = value. */
+       private static final Pattern KV_PATTERN = 
Pattern.compile("^([^=#\\s][^=]*?)\\s*[=:]\\s*(.*)$", Pattern.DOTALL);
+
+       /** Delimiter for nested section names (e.g. {@code address/street}). */
+       private static final String SECTION_PATH_DELIMITER = "/";
+
+       /**
+        * Builder for INI parser session.
+        */
+       public static class Builder extends ReaderParserSession.Builder {
+
+               protected Builder(IniParser ctx) {
+                       super(assertArgNotNull(ARG_ctx, ctx));
+               }
+
+               @Override
+               public IniParserSession build() {
+                       return new IniParserSession(this);
+               }
+       }
+
+       /**
+        * Creates a session builder.
+        *
+        * @param ctx The parser context.
+        * @return The builder.
+        */
+       public static Builder create(IniParser ctx) {
+               return new Builder(assertArgNotNull(ARG_ctx, ctx));
+       }
+
+       protected IniParserSession(Builder builder) {
+               super(builder);
+       }
+
+       @Override
+       protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws 
IOException, ParseException, ExecutableException {
+               try (Reader r = pipe.getParserReader()) {
+                       if (r == null)
+                               return null;
+                       var sections = parseIniContent(r);
+                       if (sections.isEmpty())
+                               return type.canCreateNewBean(getOuter()) ? 
type.newInstance(getOuter()) : null;
+                       if (!type.isBean() && !type.isMap())
+                               throw new ParseException(this, "INI format 
requires bean or Map<String,?> target. Got: {0}", type.inner().getName());
+                       if (type.isMap()) {
+                               var result = buildMapFromSections(sections, "");
+                               return (T) convertMapToTarget(result, type);
+                       }
+                       var bm = toBeanMap(type.newInstance(getOuter()));
+                       populateBean(bm, sections, "");
+                       return type.cast(bm.getBean());
+               }
+       }
+
+       /**
+        * Parses INI content into section -> (key -> raw value) structure.
+        *
+        * @param r The reader.
+        * @return Map of section name to key-value map. Default section uses 
"".
+        */
+       protected Map<String, Map<String, String>> parseIniContent(Reader r) 
throws IOException {
+               var sections = new LinkedHashMap<String, Map<String, String>>();
+               var current = sections.computeIfAbsent("", k -> new 
LinkedHashMap<>());
+               var br = r instanceof BufferedReader r2 ? r2 : new 
BufferedReader(r);
+               String line;
+               while ((line = br.readLine()) != null) {
+                       line = line.trim();
+                       if (line.isEmpty() || line.startsWith("#"))
+                               continue;
+                       if (line.startsWith("[")) {
+                               var end = line.indexOf(']');
+                               if (end < 0)
+                                       continue;
+                               var sectionName = line.substring(1, end).trim();
+                               current = sections.computeIfAbsent(sectionName, 
k -> new LinkedHashMap<>());
+                               continue;
+                       }
+                       var m = KV_PATTERN.matcher(line);
+                       if (m.matches()) {
+                               var key = m.group(1).trim();
+                               var value = m.group(2).trim();
+                               // Strip inline comment (# ...) from value 
(when not inside quotes)
+                               value = stripInlineComment(value);
+                               current.put(key, value);
+                       }
+               }
+               return sections;
+       }
+
+       private void populateBean(BeanMap<?> bm, Map<String, Map<String, 
String>> sections, String sectionPath) throws ParseException, 
ExecutableException {
+               var defaultSection = sections.get(sectionPath);
+               if (defaultSection != null) {
+                       for (Entry<String, String> e : 
defaultSection.entrySet()) {
+                               var key = e.getKey();
+                               var rawValue = e.getValue();
+                               var pMeta = 
bm.getMeta().getProperties().get(key);
+                               if (pMeta == null && 
isIgnoreUnknownBeanProperties())
+                                       continue;
+                               if (pMeta == null)
+                                       throw new ParseException(this, "Unknown 
property ''{0}''", key);
+                               var value = parseValue(rawValue, 
pMeta.getClassMeta());
+                               bm.put(key, value);
+                       }
+               }
+               // Process nested sections
+               for (var entry : sections.entrySet()) {
+                       var sectionName = entry.getKey();
+                       var sub = entry.getValue();
+                       if (sectionName.equals(sectionPath))
+                               continue;
+                       var isChild = sectionPath.isEmpty() ? 
!sectionName.contains(SECTION_PATH_DELIMITER)
+                               : sectionName.startsWith(sectionPath + 
SECTION_PATH_DELIMITER);
+                       if (!isChild)
+                               continue;
+                       var childName = sectionPath.isEmpty() ? sectionName
+                               : sectionName.substring(sectionPath.length() + 
SECTION_PATH_DELIMITER.length()).split(Pattern.quote(SECTION_PATH_DELIMITER))[0];
+                       var pMeta = bm.getMeta().getProperties().get(childName);
+                       if (pMeta == null && isIgnoreUnknownBeanProperties())
+                               continue;
+                       if (pMeta == null)
+                               continue;
+                       var cMeta = pMeta.getClassMeta();
+                       var childPath = sectionPath.isEmpty() ? childName : 
sectionPath + SECTION_PATH_DELIMITER + childName;
+                       if (cMeta.isBean()) {
+                               var child = 
toBeanMap(cMeta.newInstance(getOuter()));
+                               populateBean(child, sections, childPath);
+                               bm.put(childName, child.getBean());
+                       } else if (cMeta.isMap() && sub != null) {
+                               var valueType = cMeta.getValueType();
+                               if (valueType == null)
+                                       valueType = object();
+                               var map = new LinkedHashMap<String, Object>();
+                               for (Entry<String, String> e : sub.entrySet())
+                                       map.put(e.getKey(), 
parseValue(e.getValue(), valueType));
+                               bm.put(childName, convertMapToTarget(map, 
cMeta));
+                       }
+               }
+       }
+
+       private Map<String, Object> buildMapFromSections(Map<String, 
Map<String, String>> sections, String sectionPath) throws ParseException, 
ExecutableException {
+               var result = new LinkedHashMap<String, Object>();
+               var defaultSection = sections.get(sectionPath);
+               if (defaultSection != null) {
+                       for (Entry<String, String> e : 
defaultSection.entrySet())
+                               result.put(e.getKey(), parseValue(e.getValue(), 
object()));
+               }
+               for (var entry : sections.entrySet()) {
+                       var sectionName = entry.getKey();
+                       var childSection = entry.getValue();
+                       if (sectionName.equals(sectionPath))
+                               continue;
+                       var isChild = sectionPath.isEmpty() ? 
!sectionName.contains(SECTION_PATH_DELIMITER)
+                               : sectionName.startsWith(sectionPath + 
SECTION_PATH_DELIMITER);
+                       if (!isChild)
+                               continue;
+                       var childName = sectionPath.isEmpty() ? sectionName
+                               : sectionName.substring(sectionPath.length() + 
SECTION_PATH_DELIMITER.length()).split(Pattern.quote(SECTION_PATH_DELIMITER))[0];
+                       if (result.containsKey(childName))
+                               continue;
+                       var childPath = sectionPath.isEmpty() ? childName : 
sectionPath + SECTION_PATH_DELIMITER + childName;
+                       var hasNested = sections.keySet().stream().anyMatch(s 
-> s.startsWith(childPath + SECTION_PATH_DELIMITER));
+                       if (hasNested) {
+                               result.put(childName, 
buildMapFromSections(sections, childPath));
+                       } else if (childSection != null) {
+                               var map = new LinkedHashMap<String, Object>();
+                               for (Entry<String, String> e : 
childSection.entrySet())
+                                       map.put(e.getKey(), 
parseValue(e.getValue(), object()));
+                               result.put(childName, map);
+                       }
+               }
+               return result;
+       }
+
+       private Object parseValue(String raw, ClassMeta<?> targetType) throws 
ParseException, ExecutableException {
+               if (raw == null)
+                       return null;
+               var trimmed = raw.trim();
+               if (trimmed.equals("null"))
+                       return null;
+               if (trimmed.equalsIgnoreCase("true"))
+                       return true;
+               if (trimmed.equalsIgnoreCase("false"))
+                       return false;
+               if (trimmed.startsWith("'") && trimmed.endsWith("'") && 
trimmed.length() >= 2) {
+                       var inner = trimmed.substring(1, trimmed.length() - 
1).replace("''", "'");
+                       return convertToMemberType(null, inner, targetType);
+               }
+               if (trimmed.startsWith("[") || trimmed.startsWith("{"))
+                       return getJsonParser().parse(trimmed, targetType);
+               if (targetType.isNumber()) {
+                       try {
+                               if (trimmed.contains(".") || 
trimmed.toLowerCase().contains("e"))
+                                       return Double.parseDouble(trimmed);
+                               return Long.parseLong(trimmed);
+                       } catch (@SuppressWarnings("unused") 
NumberFormatException e) {
+                               return convertToMemberType(null, trimmed, 
targetType);
+                       }
+               }
+               if (targetType.isDateOrCalendarOrTemporal() || 
targetType.isDuration())
+                       return Iso8601Utils.parse(trimmed, targetType, 
getTimeZone());
+               return convertToMemberType(null, trimmed, targetType);
+       }
+
+       private static JsonParser getJsonParser() {
+               return Json5Parser.DEFAULT;
+       }
+
+       private static Object convertMapToTarget(Map<String, Object> map, 
ClassMeta<?> type) throws ParseException, ExecutableException {
+               if (JsonMap.class.isAssignableFrom(type.inner()))
+                       return new JsonMap(map);
+               return map;
+       }
+
+       private static String stripInlineComment(String value) {
+               if (value == null || !value.contains("#"))
+                       return value;
+               var inQuote = false;
+               for (var i = 0; i < value.length(); i++) {
+                       var c = value.charAt(i);
+                       if (c == '\'')
+                               inQuote = !inQuote;
+                       else if (c == '#' && !inQuote)
+                               return value.substring(0, i).trim();
+               }
+               return value;
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniSerializer.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniSerializer.java
new file mode 100644
index 0000000000..770e49f167
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniSerializer.java
@@ -0,0 +1,276 @@
+/*
+ * 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.ini;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Serializes POJO models to INI format.
+ *
+ * <p>
+ * Converts Java POJOs to INI format (section/key-value structure).
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Produces: <bc>text/ini</bc>
+ * <br>Accepts: <bc>text/ini, text/x-ini</bc>
+ *
+ * <h5 class='topic'>Bean-to-INI mapping</h5>
+ * <ul class='spaced-list'>
+ *     <li>Simple properties → key-value pairs in the default section
+ *     <li>Nested beans → <c>[sectionName]</c> sections
+ *     <li>Deeply nested beans → <c>[path/to/section]</c> section paths
+ *     <li>Collections and complex values → JSON5-encoded inline values
+ *     <li>Maps with string keys → <c>[sectionName]</c> sections
+ *     <li>Null values → unquoted <c>null</c> token
+ * </ul>
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     <jc>// Serialize a bean to INI</jc>
+ *     String <jv>ini</jv> = 
IniSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>myBean</jv>);
+ *
+ *     <jc>// Create a custom serializer</jc>
+ *     IniSerializer <jv>s</jv> = 
IniSerializer.<jsm>create</jsm>().useComments().build();
+ *     <jv>ini</jv> = <jv>s</jv>.serialize(<jv>myBean</jv>);
+ * </p>
+ *
+ * <h5 class='figure'>Example output (Map of name/age):</h5>
+ * <p class='bini'>
+ *     <ck>name</ck> = <cv>Alice</cv>
+ *     <ck>age</ck> = <cv>30</cv>
+ * </p>
+ *
+ * <h5 class='figure'>Complex (nested object + array):</h5>
+ * <p class='bini'>
+ *     <ck>name</ck> = <cv>Alice</cv>
+ *     <ck>age</ck> = <cv>30</cv>
+ *     <ck>tags</ck> = <cv>['a','b','c']</cv>
+ *
+ *     <cs>[address]</cs>
+ *     <ck>street</ck> = <cv>123 Main St</cv>
+ *     <ck>city</ck> = <cv>Boston</cv>
+ *     <ck>state</ck> = <cv>MA</cv>
+ * </p>
+ *
+ * <h5 class='topic'>Limitations</h5>
+ * <p>
+ * <ul class='spaced-list'>
+ *     <li>Top-level collections, arrays, and scalar values are not supported. 
The root must be a bean or
+ *             <c>Map&lt;String,?&gt;</c>. Throws {@link SerializeException} 
for unsupported root types.
+ *     <li>{@link java.io.Reader} and {@link java.io.InputStream} passthrough 
is not supported.
+ * </ul>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ *     <li class='note'>This class is thread safe and reusable.
+ *     <li class='note'>Complex values (collections, maps with complex values) 
are embedded as JSON5 inline
+ *             strings and remain fully round-trippable.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/IniBasics";>INI Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+       "java:S110", "java:S115"
+})
+public class IniSerializer extends WriterSerializer implements IniMetaProvider 
{
+
+       private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>, 
IniClassMeta> iniClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
+       private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta, 
IniBeanPropertyMeta> iniBeanPropertyMetas = new 
java.util.concurrent.ConcurrentHashMap<>();
+
+       private static final String PROP_kvSeparator = "kvSeparator";
+       private static final String PROP_spacedSeparator = "spacedSeparator";
+       private static final String PROP_useComments = "useComments";
+       private static final String ARG_copyFrom = "copyFrom";
+
+       /**
+        * Builder class.
+        */
+       public static class Builder extends WriterSerializer.Builder {
+
+               private static final Cache<HashKey,IniSerializer> CACHE = 
Cache.of(HashKey.class, IniSerializer.class).build();
+
+               private char kvSeparator = '=';
+               private boolean spacedSeparator = true;
+               private boolean useComments = false;
+
+               protected Builder() {
+                       produces("text/ini");
+                       accept("text/ini,text/x-ini");
+               }
+
+               protected Builder(Builder copyFrom) {
+                       super(assertArgNotNull(ARG_copyFrom, copyFrom));
+                       kvSeparator = copyFrom.kvSeparator;
+                       spacedSeparator = copyFrom.spacedSeparator;
+                       useComments = copyFrom.useComments;
+               }
+
+               protected Builder(IniSerializer copyFrom) {
+                       super(assertArgNotNull(ARG_copyFrom, copyFrom));
+                       kvSeparator = copyFrom.kvSeparator;
+                       spacedSeparator = copyFrom.spacedSeparator;
+                       useComments = copyFrom.useComments;
+               }
+
+               /**
+                * Key-value separator character.
+                *
+                * @param value <c>=</c> (default) or <c>:</c>.
+                * @return This object.
+                */
+               public Builder kvSeparator(char value) {
+                       kvSeparator = value;
+                       return this;
+               }
+
+               /**
+                * Whether to add spaces around the separator.
+                *
+                * @param value <jk>true</jk> for <c>key = value</c>, 
<jk>false</jk> for <c>key=value</c>.
+                * @return This object.
+                */
+               public Builder spacedSeparator(boolean value) {
+                       spacedSeparator = value;
+                       return this;
+               }
+
+               /**
+                * Whether to emit <c>#</c> comments from bean property 
descriptions.
+                *
+                * @param value The flag.
+                * @return This object.
+                */
+               public Builder useComments(boolean value) {
+                       useComments = value;
+                       return this;
+               }
+
+               /**
+                * Emit <c>#</c> comments from bean property descriptions.
+                *
+                * @return This object.
+                */
+               public Builder useComments() {
+                       useComments = true;
+                       return this;
+               }
+
+               @Override
+               public Builder useWhitespace() {
+                       super.useWhitespace();
+                       return this;
+               }
+
+               @Override
+               public Builder useWhitespace(boolean value) {
+                       super.useWhitespace(value);
+                       return this;
+               }
+
+               @Override
+               public Builder ws() {
+                       return useWhitespace();
+               }
+
+               @Override
+               public IniSerializer build() {
+                       return cache(CACHE).build(IniSerializer.class);
+               }
+
+               @Override
+               public Builder copy() {
+                       return new Builder(this);
+               }
+
+               @Override
+               public HashKey hashKey() {
+                       return HashKey.of(super.hashKey(), kvSeparator, 
spacedSeparator, useComments);
+               }
+       }
+
+       /** Default serializer. */
+       public static final IniSerializer DEFAULT = new IniSerializer(create());
+
+       /** Default serializer with blank lines between sections. */
+       public static final IniSerializer DEFAULT_READABLE = new 
IniSerializer(create().ws());
+
+       /**
+        * Creates a new builder.
+        *
+        * @return A new builder.
+        */
+       public static Builder create() {
+               return new Builder();
+       }
+
+       /** Key-value separator. */
+       protected final char kvSeparator;
+
+       /** Whether to add spaces around the separator. */
+       protected final boolean spacedSeparator;
+
+       /** Whether to emit property descriptions as comments. */
+       protected final boolean useComments;
+
+       /**
+        * Constructor.
+        *
+        * @param builder The builder.
+        */
+       public IniSerializer(Builder builder) {
+               super(builder);
+               kvSeparator = builder.kvSeparator;
+               spacedSeparator = builder.spacedSeparator;
+               useComments = builder.useComments;
+       }
+
+       @Override
+       public IniSerializerSession.Builder createSession() {
+               return IniSerializerSession.create(this);
+       }
+
+       @Override
+       public Builder copy() {
+               return new Builder(this);
+       }
+
+       @Override
+       public IniBeanPropertyMeta getIniBeanPropertyMeta(BeanPropertyMeta bpm) 
{
+               if (bpm == null)
+                       return IniBeanPropertyMeta.DEFAULT;
+               return iniBeanPropertyMetas.computeIfAbsent(bpm, k -> new 
IniBeanPropertyMeta(k, this));
+       }
+
+       @Override
+       public IniClassMeta getIniClassMeta(ClassMeta<?> cm) {
+               return iniClassMetas.computeIfAbsent(cm, k -> new 
IniClassMeta(k, this));
+       }
+
+       @Override
+       protected FluentMap<String,Object> properties() {
+               return super.properties()
+                       .a(PROP_kvSeparator, String.valueOf(kvSeparator))
+                       .a(PROP_spacedSeparator, spacedSeparator)
+                       .a(PROP_useComments, useComments);
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniSerializerSession.java
new file mode 100644
index 0000000000..1918aa50a4
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniSerializerSession.java
@@ -0,0 +1,330 @@
+/*
+ * 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.ini;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.function.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.utils.Iso8601Utils;
+
+/**
+ * Session for serializing objects to INI format.
+ */
+@SuppressWarnings({
+       "resource", // IniWriter lifecycle managed by SerializerPipe
+       "java:S110", // Inheritance depth acceptable for serializer session 
hierarchy
+       "java:S3776", // Cognitive complexity acceptable for serialization logic
+       "java:S6541"  // Acceptable for session implementation
+})
+public class IniSerializerSession extends WriterSerializerSession {
+
+       private static final String ARG_ctx = "ctx";
+
+       /**
+        * Builder for INI serializer session.
+        */
+       public static class Builder extends WriterSerializerSession.Builder {
+
+               private IniSerializer ctx;
+
+               protected Builder(IniSerializer ctx) {
+                       super(assertArgNotNull(ARG_ctx, ctx));
+                       this.ctx = ctx;
+               }
+
+               @Override
+               public IniSerializerSession build() {
+                       return new IniSerializerSession(this);
+               }
+       }
+
+       /**
+        * Creates a session builder.
+        *
+        * @param ctx The serializer context.
+        * @return The builder.
+        */
+       public static Builder create(IniSerializer ctx) {
+               return new Builder(assertArgNotNull(ARG_ctx, ctx));
+       }
+
+       private final IniSerializer ctx;
+
+       protected IniSerializerSession(Builder builder) {
+               super(builder);
+               ctx = builder.ctx;
+       }
+
+       @Override
+       protected void doSerialize(SerializerPipe out, Object o) throws 
IOException, SerializeException {
+               if (o == null)
+                       return;
+               var aType = getClassMetaForObject(o);
+               if (!aType.isBean() && !aType.isMap())
+                       throw new SerializeException(this, "INI format requires 
a bean or Map<String,?> at root. Got: {0}", aType.inner().getName());
+               var w = getIniWriter(out);
+               var eType = getExpectedRootType(o);
+               if (isAddBeanTypes() || isAddRootType()) {
+                       var typeName = getBeanTypeName(this, eType, aType, 
null);
+                       if (typeName == null)
+                               typeName = aType.inner().getSimpleName();
+                       var typeKey = getBeanTypePropertyName(eType);
+                       w.keyValue(typeKey, typeName);
+               }
+               if (aType.isBean())
+                       serializeBean(w, toBeanMap(o), "");
+               else
+                       serializeMapAtRoot(w, (Map<?,?>)o, eType);
+       }
+
+       protected final IniWriter getIniWriter(SerializerPipe out) {
+               var output = out.getRawOutput();
+               if (output instanceof IniWriter w)
+                       return w;
+               var w = new IniWriter(out.getWriter(), isUseWhitespace(), 
getMaxIndent(), isTrimStrings(),
+                       ctx.kvSeparator, ctx.spacedSeparator, getUriResolver());
+               out.setWriter(w);
+               return w;
+       }
+
+       private void serializeBean(IniWriter w, BeanMap<?> m, String 
sectionPath) throws IOException, SerializeException {
+               Predicate<Object> checkNull = x -> isKeepNullProperties() || 
nn(x);
+               var simple = new ArrayList<Map.Entry<BeanPropertyMeta, 
Object>>();
+               var sections = new ArrayList<Map.Entry<BeanPropertyMeta, 
Object>>();
+
+               m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
+                       if (nn(thrown))
+                               onBeanGetterException(pMeta, thrown);
+                       if (canIgnoreValue(pMeta.getClassMeta(), key, value))
+                               return;
+                       var cMeta = pMeta.getClassMeta();
+                       var aType = value == null ? cMeta : 
getClassMetaForObject(value, cMeta);
+                       var iniMeta = ctx.getIniBeanPropertyMeta(pMeta);
+                       // Collections/arrays are written as inline key-value 
(not sections), so they must appear in
+                       // the default section before any [section] headers for 
correct parsing
+                       if (iniMeta.isJson5Encoding() || 
isSimpleOrJson5Inline(aType, value)
+                               || aType.isCollectionOrArrayOrOptional()) {
+                               simple.add(new AbstractMap.SimpleEntry<>(pMeta, 
value));
+                       } else {
+                               sections.add(new 
AbstractMap.SimpleEntry<>(pMeta, value));
+                       }
+               });
+
+               // Pass 1: simple properties and JSON5-inline values
+               for (var e : simple) {
+                       BeanPropertyMeta pMeta = e.getKey();
+                       if (ctx.useComments) {
+                               var iniMeta = ctx.getIniBeanPropertyMeta(pMeta);
+                               if (ne(iniMeta.getComment()))
+                                       w.comment(iniMeta.getComment());
+                       }
+                       writeKeyValue(w, pMeta.getName(), e.getValue(), pMeta);
+               }
+
+               // Pass 2: bean and Map sections
+               for (var e : sections) {
+                       BeanPropertyMeta pMeta = e.getKey();
+                       Object value = e.getValue();
+                       var iniMeta = ctx.getIniBeanPropertyMeta(pMeta);
+                       var key = ne(iniMeta.getSection()) ? 
iniMeta.getSection() : pMeta.getName();
+                       var cMeta = pMeta.getClassMeta();
+                       var newPath = sectionPath.isEmpty() ? key : sectionPath 
+ "/" + key;
+
+                       if (nn(value)) {
+                               if (isUseWhitespace() && !sectionPath.isEmpty())
+                                       w.blankLine();
+                               var aType = getClassMetaForObject(value, cMeta);
+                               if (aType.isBean()) {
+                                       w.section(newPath);
+                                       serializeBean(w, toBeanMap(value), 
newPath);
+                               } else if (aType.isMap()) {
+                                       var map = (Map<?,?>)value;
+                                       if (isSimpleMap(map, aType)) {
+                                               w.section(newPath);
+                                               serializeMapSection(w, map, 
aType);
+                                       } else {
+                                               if (ctx.useComments && 
ne(iniMeta.getComment()))
+                                                       
w.comment(iniMeta.getComment());
+                                               writeKeyValue(w, 
pMeta.getName(), value, pMeta);
+                                       }
+                               } else if (aType.isCollection() || 
aType.isArray()) {
+                                       if (ctx.useComments && 
ne(iniMeta.getComment()))
+                                               w.comment(iniMeta.getComment());
+                                       writeKeyValue(w, pMeta.getName(), 
value, pMeta);
+                               }
+                       } else if (isKeepNullProperties()) {
+                               if (ctx.useComments && ne(iniMeta.getComment()))
+                                       w.comment(iniMeta.getComment());
+                               writeKeyValue(w, pMeta.getName(), null, pMeta);
+                       }
+               }
+       }
+
+       private void serializeMapAtRoot(IniWriter w, Map<?,?> map, ClassMeta<?> 
type) throws SerializeException {
+               Predicate<Object> checkNull = x -> isKeepNullProperties() || 
nn(x);
+               forEachEntry(map, e -> {
+                       var k = toString(e.getKey());
+                       var v = e.getValue();
+                       if (!checkNull.test(v))
+                               return;
+                       try {
+                               var aType = getClassMetaForObject(v, type);
+                               if (aType.isBean()) {
+                                       if (isUseWhitespace())
+                                               w.blankLine();
+                                       w.section(k);
+                                       serializeBean(w, toBeanMap(v), k);
+                               } else if (aType.isMap() && v != null) {
+                                       var nested = (Map<?,?>)v;
+                                       if (isSimpleMap(nested, aType)) {
+                                               if (isUseWhitespace())
+                                                       w.blankLine();
+                                               w.section(k);
+                                               serializeMapSection(w, nested, 
aType);
+                                       } else {
+                                               writeKeyValue(w, k, v, null);
+                                       }
+                               } else {
+                                       writeKeyValue(w, k, v, null);
+                               }
+                       } catch (SerializeException | IOException ex) {
+                               throw new RuntimeException(ex);
+                       }
+               });
+       }
+
+       private void serializeMapSection(IniWriter w, Map<?,?> map, 
ClassMeta<?> type) throws SerializeException {
+               Predicate<Object> checkNull = x -> isKeepNullProperties() || 
nn(x);
+               forEachEntry(map, e -> {
+                       var k = toString(e.getKey());
+                       var v = e.getValue();
+                       if (!checkNull.test(v))
+                               return;
+                       try {
+                               writeKeyValue(w, k, v, null);
+                       } catch (SerializeException ex) {
+                               throw new RuntimeException(ex);
+                       }
+               });
+       }
+
+       private void writeKeyValue(IniWriter w, String key, Object value, 
BeanPropertyMeta pMeta) throws SerializeException {
+               var cMeta = pMeta != null ? pMeta.getClassMeta() : (value != 
null ? getClassMetaForObject(value) : object());
+               var aType = value == null ? cMeta : 
getClassMetaForObject(value, cMeta);
+
+               var swap = aType.getSwap(this);
+               if (nn(swap)) {
+                       value = swap(swap, value);
+                       aType = swap.getSwapClassMeta(this);
+                       if (aType.isObject())
+                               aType = getClassMetaForObject(value);
+               }
+
+               if (value == null && !isKeepNullProperties())
+                       return;
+
+               var valueStr = value == null ? "null"
+                       : isSimpleOrJson5Inline(aType, value) ? 
formatSimpleValue(value, aType)
+                       : encodeComplexValue(value);
+               w.keyValue(key, valueStr);
+       }
+
+       private String formatSimpleValue(Object value, ClassMeta<?> aType) 
throws SerializeException {
+               if (aType.isNumber())
+                       return value.toString();
+               if (aType.isBoolean())
+                       return ((Boolean)value).toString();
+               if (aType.isDateOrCalendarOrTemporal())
+                       return Iso8601Utils.format(value, aType, getTimeZone());
+               if (aType.isDuration())
+                       return value.toString();
+               if (aType.isEnum())
+                       return ((Enum<?>)value).name();
+               if (aType.isCharSequence() || aType.isUri()) {
+                       var s = toString(value);
+                       return needsQuoting(s) ? "'" + s.replace("'", "''") + 
"'" : s;
+               }
+               return toString(value);
+       }
+
+       private static boolean needsQuoting(String s) {
+               if (s == null || s.isEmpty())
+                       return true;
+               if (s.equals("null") || s.equalsIgnoreCase("true") || 
s.equalsIgnoreCase("false"))
+                       return true;
+               if (s.matches("-?\\d+(\\.\\d+)?([eE][+-]?\\d+)?"))
+                       return true;
+               if (s.contains("=") || s.contains("[") || s.contains("]") || 
s.contains("#") || s.contains("\n"))
+                       return true;
+               var trimmed = s.trim();
+               if (!trimmed.equals(s))
+                       return true;
+               return false;
+       }
+
+       private String encodeComplexValue(Object value) throws 
SerializeException {
+               var json5 = getJson5Serializer().serialize(value);
+               return json5;
+       }
+
+       private JsonSerializer getJson5Serializer() {
+               var b = 
Json5Serializer.create().beanContext((BeanContext)getContext());
+               if (isAddBeanTypes())
+                       b.addBeanTypes();
+               if (isAddRootType())
+                       b.addRootType();
+               return b.build();
+       }
+
+       private static boolean isSimpleOrJson5Inline(ClassMeta<?> aType, Object 
value) {
+               if (aType.isBean() || aType.isMap())
+                       return false;
+               if (aType.isCollection() || aType.isArray())
+                       return false;
+               if (aType.isStreamable())
+                       return false;
+               return true;
+       }
+
+       private boolean isSimpleMap(Map<?,?> map, ClassMeta<?> mapType) {
+               if (map == null)
+                       return false;
+               var keyType = mapType.getKeyType();
+               // When keyType is null or Object, check keys at runtime; 
otherwise require CharSequence
+               if (keyType != null && !keyType.isCharSequence() && 
!keyType.isObject())
+                       return false;
+               for (Object k : map.keySet()) {
+                       if (k != null && !(k instanceof CharSequence))
+                               return false;
+               }
+               for (Object v : map.values()) {
+                       if (v != null) {
+                               ClassMeta<?> vType = getClassMetaForObject(v);
+                               if (vType.isBean() || vType.isMap() || 
vType.isCollectionOrArray())
+                                       return false;
+                       }
+               }
+               return true;
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniWriter.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniWriter.java
new file mode 100644
index 0000000000..801ac1287d
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/IniWriter.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ini;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Specialized writer for serializing INI format.
+ *
+ * <p>
+ * Extends {@link SerializerWriter} with INI-specific methods for sections, 
key-value pairs,
+ * comments, and quoted strings.
+ *
+ * <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/IniBasics";>INI Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+       "resource" // Writer resource managed by calling code
+})
+public class IniWriter extends SerializerWriter {
+
+       /** Key-value separator character. */
+       protected final char kvSeparator;
+
+       /** Whether to add spaces around the separator. */
+       protected final boolean spacedSeparator;
+
+       /**
+        * Constructor.
+        *
+        * @param out The writer being wrapped.
+        * @param useWhitespace If <jk>true</jk>, use blank lines between 
sections.
+        * @param maxIndent The maximum indentation level (unused for INI).
+        * @param trimStrings If <jk>true</jk>, trim strings before 
serialization.
+        * @param kvSeparator The key-value separator ('=' or ':').
+        * @param spacedSeparator If <jk>true</jk>, add spaces around separator.
+        * @param uriResolver The URI resolver.
+        */
+       protected IniWriter(Writer out, boolean useWhitespace, int maxIndent, 
boolean trimStrings, char kvSeparator, boolean spacedSeparator, UriResolver 
uriResolver) {
+               super(out, useWhitespace, maxIndent, trimStrings, '\'', 
uriResolver);
+               this.kvSeparator = kvSeparator;
+               this.spacedSeparator = spacedSeparator;
+       }
+
+       /**
+        * Writes a section header with optional preceding blank line.
+        *
+        * @param name The section name or path (e.g. "address" or 
"employment/company").
+        * @return This object.
+        */
+       public IniWriter section(String name) {
+               w('\n');
+               w('[').w(name).w(']').w('\n');
+               return this;
+       }
+
+       /**
+        * Writes a comment line.
+        *
+        * @param text The comment text.
+        * @return This object.
+        */
+       public IniWriter comment(String text) {
+               w("# ");
+               if (text != null && !text.isEmpty()) {
+                       var lines = text.split("\n");
+                       for (var i = 0; i < lines.length; i++) {
+                               if (i > 0)
+                                       w("\n# ");
+                               w(lines[i]);
+                       }
+               }
+               w('\n');
+               return this;
+       }
+
+       /**
+        * Writes a key-value pair.
+        *
+        * @param key The key name.
+        * @param value The value (already formatted as string).
+        * @return This object.
+        */
+       public IniWriter keyValue(String key, String value) {
+               w(key);
+               if (spacedSeparator)
+                       w(" ").w(kvSeparator).w(" ");
+               else
+                       w(kvSeparator);
+               w(value != null ? value : "null");
+               w('\n');
+               return this;
+       }
+
+       /**
+        * Writes a single-quoted string value with escaping.
+        *
+        * <p>
+        * Single quotes within the value are escaped as <c>''</c>.
+        *
+        * @param value The raw string value.
+        * @return This object.
+        */
+       public IniWriter quotedString(String value) {
+               if (value == null) {
+                       w("null");
+                       return this;
+               }
+               w('\'').w(value.replace("'", "''")).w('\'');
+               return this;
+       }
+
+       /**
+        * Writes a blank line.
+        *
+        * @return This object.
+        */
+       public IniWriter blankLine() {
+               w('\n');
+               return this;
+       }
+
+       // Override return types for chaining
+       @Override public IniWriter append(char c) { super.append(c); return 
this; }
+       @Override public IniWriter append(char[] value) { super.append(value); 
return this; }
+       @Override public IniWriter append(int indent, char c) { 
super.append(indent, c); return this; }
+       @Override public IniWriter append(int indent, String text) { 
super.append(indent, text); return this; }
+       @Override public IniWriter append(Object text) { super.append(text); 
return this; }
+       @Override public IniWriter append(String text) { super.append(text); 
return this; }
+       @Override public IniWriter appendIf(boolean b, char c) { 
super.appendIf(b, c); return this; }
+       @Override public IniWriter appendIf(boolean b, String text) { 
super.appendIf(b, text); return this; }
+       @Override public IniWriter appendln(int indent, String text) { 
super.appendln(indent, text); return this; }
+       @Override public IniWriter appendln(String text) { 
super.appendln(text); return this; }
+       @Override public IniWriter appendUri(Object value) { 
super.appendUri(value); return this; }
+       @Override public IniWriter cr(int depth) { super.cr(depth); return 
this; }
+       @Override public IniWriter cre(int depth) { super.cre(depth); return 
this; }
+       @Override public IniWriter i(int indent) { super.i(indent); return 
this; }
+       @Override public IniWriter ie(int indent) { super.ie(indent); return 
this; }
+       @Override public IniWriter nl(int indent) { super.nl(indent); return 
this; }
+       @Override public IniWriter nlIf(boolean flag, int indent) { 
super.nlIf(flag, indent); return this; }
+       @Override public IniWriter q() { super.q(); return this; }
+       @Override public IniWriter s() { super.s(); return this; }
+       @Override public IniWriter sIf(boolean flag) { super.sIf(flag); return 
this; }
+       @Override public IniWriter w(char value) { super.w(value); return this; 
}
+       @Override public IniWriter w(String value) { super.w(value); return 
this; }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/Ini.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/Ini.java
new file mode 100644
index 0000000000..1c35a085e2
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/Ini.java
@@ -0,0 +1,81 @@
+/*
+ * 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.ini.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * Annotation for customizing INI serialization and parsing behavior on 
classes, methods, and fields.
+ *
+ * <p>
+ * Can be used in the following locations:
+ * <ul>
+ *     <li>Marshalled classes/methods/fields.
+ *     <li><ja>@Rest</ja>-annotated classes and <ja>@RestOp</ja>-annotated 
methods when an {@link #on()} value is specified.
+ * </ul>
+ */
+@Documented
+@Target({ TYPE, FIELD, METHOD })
+@Retention(RUNTIME)
+@Inherited
+@Repeatable(IniAnnotation.Array.class)
+@ContextApply(IniAnnotation.Apply.class)
+public @interface Ini {
+
+       /**
+        * Custom section name override.
+        *
+        * <p>
+        * For beans: overrides the property name as the section header.
+        *
+        * @return The annotation value.
+        */
+       String section() default "";
+
+       /**
+        * Comment text to emit before this property.
+        *
+        * @return The annotation value.
+        */
+       String comment() default "";
+
+       /**
+        * Force JSON5 encoding even for simple values.
+        *
+        * @return The annotation value.
+        */
+       boolean json5Encoding() default false;
+
+       /**
+        * Dynamically apply this annotation to the specified 
classes/methods/fields.
+        *
+        * @return The annotation value.
+        */
+       String[] on() default {};
+
+       /**
+        * Dynamically apply this annotation to the specified classes.
+        *
+        * @return The annotation value.
+        */
+       Class<?>[] onClass() default {};
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/IniAnnotation.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/IniAnnotation.java
new file mode 100644
index 0000000000..4cedf4b79e
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/IniAnnotation.java
@@ -0,0 +1,66 @@
+/*
+ * 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.ini.annotation;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Utility classes and methods for the {@link Ini @Ini} annotation.
+ */
+public class IniAnnotation {
+
+       /**
+        * Applies {@link Ini} annotations to a {@link Context.Builder}.
+        */
+       public static class Apply extends AnnotationApplier<Ini, 
Context.Builder> {
+
+               /**
+                * Constructor.
+                *
+                * @param vr The resolver for resolving values in annotations.
+                */
+               public Apply(VarResolverSession vr) {
+                       super(Ini.class, Context.Builder.class, vr);
+               }
+
+               @Override /* AnnotationApplier */
+               public void apply(AnnotationInfo<Ini> ai, Context.Builder b) {
+                       // No-op: @Ini settings are read at serialization/parse 
time via IniMetaProvider.
+               }
+       }
+
+       /**
+        * A collection of {@link Ini} annotations.
+        */
+       @Documented
+       @Target({ java.lang.annotation.ElementType.TYPE, 
java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD 
})
+       @Retention(RetentionPolicy.RUNTIME)
+       @Inherited
+       public @interface Array {
+
+               /**
+                * The child annotations.
+                *
+                * @return The annotation value.
+                */
+               Ini[] value();
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/IniConfig.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/IniConfig.java
new file mode 100644
index 0000000000..3d9d33d4e6
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/IniConfig.java
@@ -0,0 +1,81 @@
+/*
+ * 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.ini.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.ini.*;
+
+/**
+ * Annotation for specifying config properties defined in {@link 
IniSerializer} and {@link IniParser}.
+ *
+ * <p>
+ * Used primarily for specifying bean configuration properties on REST classes 
and methods.
+ */
+@Target({ TYPE, METHOD })
+@Retention(RUNTIME)
+@Inherited
+@ContextApply({ IniConfigAnnotation.SerializerApply.class, 
IniConfigAnnotation.ParserApply.class })
+public @interface IniConfig {
+
+       /**
+        * Key-value separator character.
+        *
+        * <ul class='values'>
+        *      <li><js>"="</js> (default)
+        *      <li><js>":"</js>
+        * </ul>
+        *
+        * @return The annotation value.
+        */
+       String kvSeparator() default "";
+
+       /**
+        * Whether to add spaces around the separator.
+        *
+        * <ul class='values'>
+        *      <li><js>"true"</js> (default) — <c>key = value</c>
+        *      <li><js>"false"</js> — <c>key=value</c>
+        * </ul>
+        *
+        * @return The annotation value.
+        */
+       String spacedSeparator() default "";
+
+       /**
+        * Whether to emit property descriptions as <c>#</c> comments.
+        *
+        * <ul class='values'>
+        *      <li><js>"true"</js>
+        *      <li><js>"false"</js> (default)
+        * </ul>
+        *
+        * @return The annotation value.
+        */
+       String useComments() default "";
+
+       /**
+        * Optional rank for this config.
+        *
+        * @return The annotation value.
+        */
+       int rank() default 0;
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/IniConfigAnnotation.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/IniConfigAnnotation.java
new file mode 100644
index 0000000000..257ad458f3
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/annotation/IniConfigAnnotation.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.ini.annotation;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.ini.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Utility classes and methods for the {@link IniConfig @IniConfig} annotation.
+ */
+public class IniConfigAnnotation {
+
+       private IniConfigAnnotation() {}
+
+       /**
+        * Applies {@link IniConfig} annotations to an {@link 
IniParser.Builder}.
+        */
+       public static class ParserApply extends AnnotationApplier<IniConfig, 
IniParser.Builder> {
+
+               /**
+                * Constructor.
+                *
+                * @param vr The resolver for resolving values in annotations.
+                */
+               public ParserApply(VarResolverSession vr) {
+                       super(IniConfig.class, IniParser.Builder.class, vr);
+               }
+
+               @Override
+               public void apply(AnnotationInfo<IniConfig> ai, 
IniParser.Builder b) {
+                       // No-op: Parser accepts both = and :; no 
format-specific settings needed.
+               }
+       }
+
+       /**
+        * Applies {@link IniConfig} annotations to an {@link 
IniSerializer.Builder}.
+        */
+       public static class SerializerApply extends 
AnnotationApplier<IniConfig, IniSerializer.Builder> {
+
+               /**
+                * Constructor.
+                *
+                * @param vr The resolver for resolving values in annotations.
+                */
+               public SerializerApply(VarResolverSession vr) {
+                       super(IniConfig.class, IniSerializer.Builder.class, vr);
+               }
+
+               @Override
+               public void apply(AnnotationInfo<IniConfig> ai, 
IniSerializer.Builder b) {
+                       IniConfig a = ai.inner();
+                       string(a.kvSeparator()).filter(s -> 
!s.isEmpty()).ifPresent(s -> b.kvSeparator(s.charAt(0)));
+                       bool(a.spacedSeparator()).ifPresent(b::spacedSeparator);
+                       bool(a.useComments()).ifPresent(b::useComments);
+               }
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/package-info.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/package-info.java
new file mode 100644
index 0000000000..d1caa25dc5
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ini/package-info.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+/**
+ * INI format serializer and parser for Apache Juneau.
+ *
+ * <p>
+ * INI is a classic configuration file format with sections 
(<c>[sectionName]</c>) and key-value pairs.
+ * Beans map to INI with simple properties as key-value pairs and nested beans 
as named sections.
+ *
+ * <h5 class='section'>Usage:</h5>
+ * <p class='bjava'>
+ *     String ini = 
IniSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>myBean</jv>);
+ *     MyBean bean = IniParser.<jsf>DEFAULT</jsf>.parse(<jv>ini</jv>, 
MyBean.<jk>class</jk>);
+ *
+ *     <jc>// Or use the marshaller:</jc>
+ *     String ini = Ini.<jsf>of</jsf>(<jv>myBean</jv>);
+ *     MyBean bean = Ini.<jsf>to</jsf>(<jv>ini</jv>, MyBean.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/IniBasics";>INI Basics</a>
+ * </ul>
+ */
+package org.apache.juneau.ini;
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Ini.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Ini.java
new file mode 100644
index 0000000000..77d7002bce
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Ini.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.marshaller;
+
+import java.io.*;
+import java.lang.reflect.*;
+
+import org.apache.juneau.ini.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * A pairing of an {@link IniSerializer} and {@link IniParser} 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>
+ *     Ini <jv>ini</jv> = <jk>new</jk> Ini();
+ *     MyPojo <jv>myPojo</jv> = <jv>ini</jv>.read(<jv>string</jv>, 
MyPojo.<jk>class</jk>);
+ *     String <jv>string</jv> = <jv>ini</jv>.write(<jv>myPojo</jv>);
+ * </p>
+ * <p class='bjava'>
+ *     <jc>// Using DEFAULT instance.</jc>
+ *     MyPojo <jv>myPojo</jv> = Ini.<jsf>DEFAULT</jsf>.read(<jv>string</jv>, 
MyPojo.<jk>class</jk>);
+ *     String <jv>string</jv> = Ini.<jsf>DEFAULT</jsf>.write(<jv>myPojo</jv>);
+ * </p>
+ *
+ * <h5 class='figure'>Example output (Map of name/age):</h5>
+ * <p class='bini'>
+ *     <ck>name</ck> = <cv>Alice</cv>
+ *     <ck>age</ck> = <cv>30</cv>
+ * </p>
+ *
+ * <h5 class='figure'>Complex (nested object + array):</h5>
+ * <p class='bini'>
+ *     <ck>name</ck> = <cv>Alice</cv>
+ *     <ck>age</ck> = <cv>30</cv>
+ *     <ck>tags</ck> = <cv>['a','b','c']</cv>
+ *
+ *     <cs>[address]</cs>
+ *     <ck>street</ck> = <cv>123 Main St</cv>
+ *     <ck>city</ck> = <cv>Boston</cv>
+ *     <ck>state</ck> = <cv>MA</cv>
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ *     <li class='note'>The top-level object must be a bean or 
<c>Map&lt;String,?&gt;</c>.
+ *     <li class='note'>Collections and complex map values are embedded as 
JSON5 inline strings.
+ * </ul>
+ *
+ * <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 Ini extends CharMarshaller {
+
+       /** Default reusable instance. */
+       public static final Ini DEFAULT = new Ini();
+
+       /** Default reusable instance, readable format. */
+       public static final Ini DEFAULT_READABLE = new 
Ini(IniSerializer.DEFAULT_READABLE, IniParser.DEFAULT);
+
+       /**
+        * Serializes a Java object to an INI string.
+        *
+        * @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 INI output.
+        *
+        * @param object The object to serialize.
+        * @param output The output (Writer, OutputStream, File, StringBuilder).
+        * @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 INI input to the specified Java type.
+        *
+        * @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.
+        * @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 INI input to the specified Java type.
+        *
+        * @param <T> The class type of the object to create.
+        * @param input The input.
+        * @param type The object type to create.
+        * @param args The type arguments for maps and collections.
+        * @return The parsed object.
+        * @throws ParseException Malformed input encountered.
+        * @throws IOException Thrown by underlying stream.
+        */
+       public static <T> T to(Object input, Type type, Type...args) throws 
ParseException, IOException {
+               return DEFAULT.read(input, type, args);
+       }
+
+       /**
+        * Parses an INI string to the specified type.
+        *
+        * @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 an INI string to the specified Java type.
+        *
+        * @param <T> The class type of the object to create.
+        * @param input The input.
+        * @param type The object type to create.
+        * @param args The type arguments for maps and collections.
+        * @return The parsed object.
+        * @throws ParseException Malformed input encountered.
+        */
+       public static <T> T to(String input, Type type, Type...args) throws 
ParseException {
+               return DEFAULT.read(input, type, args);
+       }
+
+       /**
+        * Constructor.
+        *
+        * <p>
+        * Uses {@link IniSerializer#DEFAULT} and {@link IniParser#DEFAULT}.
+        */
+       public Ini() {
+               this(IniSerializer.DEFAULT, IniParser.DEFAULT);
+       }
+
+       /**
+        * Constructor.
+        *
+        * @param s The serializer to use.
+        * @param p The parser to use.
+        */
+       public Ini(IniSerializer s, IniParser p) {
+               super(s, p);
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
index ef62eda74d..d7ffe9cb1c 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
@@ -92,6 +92,7 @@ import org.apache.juneau.parser.ParseException;
 import org.apache.juneau.plaintext.*;
 import org.apache.juneau.rest.client.assertion.*;
 import org.apache.juneau.toml.*;
+import org.apache.juneau.ini.*;
 import org.apache.juneau.proto.*;
 import org.apache.juneau.rest.client.remote.*;
 import org.apache.juneau.serializer.*;
@@ -5567,6 +5568,7 @@ public class RestClient extends BeanContextable 
implements HttpClient, Closeable
                                        MsgPackSerializer.class,
                                        PlainTextSerializer.class,
                                        TomlSerializer.class,
+                                       IniSerializer.class,
                                        ProtoSerializer.class,
                                        MarkdownSerializer.class
                                )
@@ -5582,6 +5584,7 @@ public class RestClient extends BeanContextable 
implements HttpClient, Closeable
                                        MsgPackParser.class,
                                        PlainTextParser.class,
                                        TomlParser.class,
+                                       IniParser.class,
                                        ProtoParser.class,
                                        MarkdownParser.class
                                );
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
index 47fc894764..7d2bf82567 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
@@ -31,6 +31,7 @@ import org.apache.juneau.serializer.annotation.*;
 import org.apache.juneau.soap.*;
 import org.apache.juneau.yaml.*;
 import org.apache.juneau.toml.*;
+import org.apache.juneau.ini.*;
 import org.apache.juneau.proto.*;
 import org.apache.juneau.markdown.*;
 import org.apache.juneau.uon.*;
@@ -154,6 +155,7 @@ import org.apache.juneau.xml.*;
                CsvSerializer.class,
                YamlSerializer.class,
                TomlSerializer.class,
+               IniSerializer.class,
                ProtoSerializer.class,
                MarkdownSerializer.class
        },
@@ -173,6 +175,7 @@ import org.apache.juneau.xml.*;
                CsvParser.class,
                YamlParser.class,
                TomlParser.class,
+               IniParser.class,
                ProtoParser.class,
                MarkdownParser.class
        }
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 2ccc2edb0a..96c77d4db2 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
@@ -39,6 +39,7 @@ import org.apache.juneau.urlencoding.*;
 import org.apache.juneau.xml.*;
 import org.apache.juneau.yaml.*;
 import org.apache.juneau.toml.*;
+import org.apache.juneau.ini.*;
 import org.apache.juneau.markdown.*;
 
 /**
@@ -143,6 +144,7 @@ public class ComboRoundTrip_Tester<T> {
                public Builder<T> yamlR(String value) { expected.put("yamlR", 
value); return this; }
                public Builder<T> csv(String value) { expected.put("csv", 
value); return this; }
                public Builder<T> toml(String value) { expected.put("toml", 
value); return this; }
+               public Builder<T> ini(String value) { expected.put("ini", 
value); return this; }
                public Builder<T> markdown(String value) { 
expected.put("markdown", value); return this; }
 
                public ComboRoundTrip_Tester<T> build() {
@@ -211,6 +213,7 @@ public class ComboRoundTrip_Tester<T> {
                serializers.put("yamlR", create(b, 
YamlSerializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
                serializers.put("csv", create(b, CsvSerializer.create()));
                serializers.put("toml", create(b, 
TomlSerializer.DEFAULT.copy().addBeanTypes().addRootType()));
+               serializers.put("ini", create(b, 
IniSerializer.DEFAULT.copy().addBeanTypes().addRootType()));
                serializers.put("markdown", create(b, 
MarkdownSerializer.create().keepNullProperties().addBeanTypes().addRootType()));
 
                parsers.put("json", create(b, JsonParser.DEFAULT.copy()));
@@ -253,6 +256,7 @@ public class ComboRoundTrip_Tester<T> {
                parsers.put("yamlR", create(b, YamlParser.DEFAULT.copy()));
                parsers.put("csv", create(b, CsvParser.create()));
                parsers.put("toml", create(b, TomlParser.DEFAULT.copy()));
+               parsers.put("ini", create(b, IniParser.DEFAULT.copy()));
                parsers.put("markdown", create(b, MarkdownParser.create()));
        }
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java 
b/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java
index 71f1a53012..0eb4590e5c 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java
@@ -38,6 +38,7 @@ import org.apache.juneau.urlencoding.*;
 import org.apache.juneau.xml.*;
 import org.apache.juneau.yaml.*;
 import org.apache.juneau.toml.*;
+import org.apache.juneau.ini.*;
 import org.apache.juneau.markdown.*;
 
 /**
@@ -128,6 +129,7 @@ public class ComboSerialize_Tester<T> {
                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 Builder<T> toml(String value) { expected.put("toml", 
value); return this; }
+               public Builder<T> ini(String value) { expected.put("ini", 
value); return this; }
                public Builder<T> markdown(String value) { 
expected.put("markdown", value); return this; }
 
                public ComboSerialize_Tester<T> build() {
@@ -189,6 +191,7 @@ public class ComboSerialize_Tester<T> {
                serializers.put("yamlT", create(b, 
YamlSerializer.create().typePropertyName("t").addBeanTypes().addRootType()));
                serializers.put("yamlR", create(b, 
YamlSerializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
                serializers.put("toml", create(b, 
TomlSerializer.DEFAULT.copy().addBeanTypes().addRootType()));
+               serializers.put("ini", create(b, 
IniSerializer.DEFAULT.copy().addBeanTypes().addRootType()));
                serializers.put("markdown", create(b, 
MarkdownSerializer.create().keepNullProperties().addBeanTypes().addRootType()));
        }
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
index 107ea66fc2..99695bdba8 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
@@ -36,6 +36,7 @@ import org.apache.juneau.urlencoding.*;
 import org.apache.juneau.xml.*;
 import org.apache.juneau.yaml.*;
 import org.apache.juneau.toml.*;
+import org.apache.juneau.ini.*;
 import org.apache.juneau.proto.*;
 import org.junit.jupiter.params.*;
 import org.junit.jupiter.params.provider.*;
@@ -176,16 +177,20 @@ class RoundTripDateTime_Test extends TestBase {
                        .serializer(TomlSerializer.create())
                        .parser(TomlParser.create())
                        .build(),
-               tester(32, "Csv - default")
+               tester(32, "Ini - default")
+                       .serializer(IniSerializer.create())
+                       .parser(IniParser.create())
+                       .build(),
+               tester(33, "Csv - default")
                        .serializer(CsvSerializer.create().keepNullProperties())
                        .skipIf(o -> o == null || (o.getClass().isArray() && 
o.getClass().getComponentType().isPrimitive()))
                        .returnOriginalObject()
                        .build(),
-               tester(33, "Markdown - default")
+               tester(34, "Markdown - default")
                        
.serializer(MarkdownSerializer.create().keepNullProperties().addBeanTypes().addRootType())
                        .parser(MarkdownParser.create())
                        .build(),
-               tester(34, "Proto - default")
+               tester(35, "Proto - default")
                        
.serializer(ProtoSerializer.create().keepNullProperties().addBeanTypes().addRootType())
                        .parser(ProtoParser.create())
                        .build(),
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ini/IniParser_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/ini/IniParser_Test.java
new file mode 100644
index 0000000000..de22e1d38f
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ini/IniParser_Test.java
@@ -0,0 +1,129 @@
+/*
+ * 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 FOR A PARTICULAR PURPOSE.  See the
+ * License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ini;
+
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link IniParser}.
+ */
+@SuppressWarnings("unchecked")
+class IniParser_Test extends TestBase {
+
+       
//====================================================================================================
+       // a - Simple flat parsing
+       
//====================================================================================================
+
+       @Test
+       void a01_simpleBean() throws Exception {
+               var ini = "host = localhost\nport = 8080\ndebug = true";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "host,port,debug", "localhost,8080,true");
+       }
+
+       @Test
+       void a02_nestedBean() throws Exception {
+               var ini = "name = myapp\n\n[database]\nhost = localhost\nport = 
5432";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "name,database{host,port}", 
"myapp,{localhost,5432}");
+       }
+
+       @Test
+       void a03_nullValues() throws Exception {
+               var ini = "name = Alice\nmiddle = null";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "name,middle", "Alice,<null>");
+       }
+
+       @Test
+       void a04_quotedStrings() throws Exception {
+               var ini = "a = 'hello'\nb = 'world'";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "a,b", "hello,world");
+       }
+
+       @Test
+       void a05_numbersAndBooleans() throws Exception {
+               var ini = "count = 42\nratio = 3.14\nflag = true";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "count,ratio,flag", "42,3.14,true");
+       }
+
+       @Test
+       void a06_listOfStrings() throws Exception {
+               var ini = "tags = ['a','b','c']";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "tags", "[a,b,c]");
+       }
+
+       @Test
+       void a07_commentsIgnored() throws Exception {
+               var ini = "# comment\nname = Alice\n# another";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "name", "Alice");
+       }
+
+       @Test
+       void a08_blankLinesIgnored() throws Exception {
+               var ini = "a = 1\n\n\nb = 2";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "a,b", "1,2");
+       }
+
+       @Test
+       void a09_deeplyNestedBean() throws Exception {
+               var ini = "name = John\n\n[employment]\ntitle = 
Engineer\n\n[employment/company]\nname = Acme\nticker = ACME";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "name,employment{title,company{name,ticker}}", 
"John,{Engineer,{Acme,ACME}}");
+       }
+
+       @Test
+       void a10_emptyInput() throws Exception {
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse("", 
Map.class, String.class, Object.class);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+       }
+
+       @Test
+       void a11_colonSeparator() throws Exception {
+               var ini = "key: value";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "key", "value");
+       }
+
+       @Test
+       void a12_noSpaceSeparator() throws Exception {
+               var ini = "key=value";
+               var m = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(m, "key", "value");
+       }
+
+       @Test
+       void a13_topLevelCollectionThrows() throws Exception {
+               var ex = assertThrows(Exception.class, () ->
+                       IniParser.DEFAULT.parse("[]", List.class, 
String.class));
+               assertTrue(ex.getMessage().contains("not supported") || 
ex.getMessage().contains("bean")
+                       || ex.getClass().getSimpleName().contains("Parse"));
+       }
+
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ini/IniRoundTrip_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/ini/IniRoundTrip_Test.java
new file mode 100644
index 0000000000..d10fdf51dd
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ini/IniRoundTrip_Test.java
@@ -0,0 +1,186 @@
+/*
+ * 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 FOR A PARTICULAR PURPOSE.  See the
+ * License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ini;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Round-trip tests for {@link IniSerializer} and {@link IniParser}.
+ */
+@SuppressWarnings({
+       "unchecked" // Parser returns Object; cast to Map/bean in tests
+})
+class IniRoundTrip_Test extends TestBase {
+
+       @Bean(properties = "name,age")
+       public static class Person {
+               public String name;
+               public int age;
+
+               public Person() {}
+               public Person(String name, int age) {
+                       this.name = name;
+                       this.age = age;
+               }
+       }
+
+       @Bean(properties = "name,address,tags")
+       public static class ComplexPerson {
+               public String name;
+               public Address address;
+               public List<String> tags;
+
+               public ComplexPerson() {}
+               public ComplexPerson(String name, Address address, List<String> 
tags) {
+                       this.name = name;
+                       this.address = address;
+                       this.tags = tags;
+               }
+       }
+
+       @Bean(properties = "street,city")
+       public static class Address {
+               public String street;
+               public String city;
+
+               public Address() {}
+               public Address(String street, String city) {
+                       this.street = street;
+                       this.city = city;
+               }
+       }
+
+       
//====================================================================================================
+       // a - Simple bean round-trip
+       
//====================================================================================================
+
+       @Test
+       void a01_simpleBeanRoundTrip() throws Exception {
+               var a = new Person("Alice", 30);
+               var ini = IniSerializer.DEFAULT.serialize(a);
+               var b = IniParser.DEFAULT.parse(ini, Person.class);
+               assertBean(b, "name,age", "Alice,30");
+       }
+
+       @Test
+       void a02_nestedBeanRoundTrip() throws Exception {
+               var a = new ComplexPerson("Alice", new Address("123 Main", 
"Boston"), list("a", "b", "c"));
+               var ini = IniSerializer.DEFAULT.serialize(a);
+               var b = IniParser.DEFAULT.parse(ini, ComplexPerson.class);
+               assertBean(b, "name,address{street,city},tags", "Alice,{123 
Main,Boston},[a,b,c]");
+       }
+
+       @Test
+       void a03_mapRoundTrip() throws Exception {
+               var a = new LinkedHashMap<String, Object>();
+               a.put("name", "test");
+               a.put("count", 42);
+               a.put("flag", true);
+               var ini = IniSerializer.DEFAULT.serialize(a);
+               var b = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(b, "name,count,flag", "test,42,true");
+       }
+
+       @Test
+       void a04_nestedMapRoundTrip() throws Exception {
+               var nested = new LinkedHashMap<String, Object>();
+               nested.put("host", "localhost");
+               nested.put("port", 8080);
+               var a = new LinkedHashMap<String, Object>();
+               a.put("name", "app");
+               a.put("database", nested);
+               var ini = IniSerializer.DEFAULT.serialize(a);
+               var b = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(b, "name,database{host,port}", 
"app,{localhost,8080}");
+       }
+
+       @Test
+       void a05_nullPropertiesRoundTrip() throws Exception {
+               var a = new LinkedHashMap<String, Object>();
+               a.put("name", "Alice");
+               a.put("middle", null);
+               var s = IniSerializer.create().keepNullProperties().build();
+               var ini = s.serialize(a);
+               var b = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(b, "name,middle", "Alice,<null>");
+       }
+
+       @Test
+       void a06_collectionsRoundTrip() throws Exception {
+               var a = new LinkedHashMap<String, Object>();
+               a.put("name", "x");
+               a.put("tags", list("a", "b", "c"));
+               a.put("counts", list(1, 2, 3));
+               var ini = IniSerializer.DEFAULT.serialize(a);
+               var b = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(b, "name,tags,counts", "x,[a,b,c],[1,2,3]");
+       }
+
+       @Test
+       void a07_deepNestingRoundTrip() throws Exception {
+               var company = new LinkedHashMap<String, Object>();
+               company.put("name", "Acme");
+               company.put("ticker", "ACME");
+               var employment = new LinkedHashMap<String, Object>();
+               employment.put("title", "Engineer");
+               employment.put("company", company);
+               var a = new LinkedHashMap<String, Object>();
+               a.put("name", "John");
+               a.put("employment", employment);
+               var ini = IniSerializer.DEFAULT.serialize(a);
+               var b = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(b, "name,employment{title,company{name,ticker}}", 
"John,{Engineer,{Acme,ACME}}");
+       }
+
+       @Test
+       void a08_emptyCollectionsRoundTrip() throws Exception {
+               var a = new LinkedHashMap<String, Object>();
+               a.put("name", "x");
+               a.put("tags", list());
+               var ini = IniSerializer.DEFAULT.serialize(a);
+               var b = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(b, "name,tags", "x,[]");
+       }
+
+       @Test
+       void a09_stringEdgeCases() throws Exception {
+               var a = new LinkedHashMap<String, Object>();
+               a.put("empty", "");
+               a.put("numeric", "123");
+               a.put("boolean", "true");
+               var ini = IniSerializer.DEFAULT.serialize(a);
+               var b = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(b, "empty,numeric,boolean", ",123,true");
+       }
+
+       @Test
+       void a10_unicodeRoundTrip() throws Exception {
+               var a = new LinkedHashMap<String, Object>();
+               a.put("name", "José");
+               a.put("emoji", "Hello \uD83D\uDE00");
+               var ini = IniSerializer.DEFAULT.serialize(a);
+               var b = (Map<String, Object>) IniParser.DEFAULT.parse(ini, 
Map.class, String.class, Object.class);
+               assertBean(b, "name,emoji", "José,Hello \uD83D\uDE00");
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ini/IniSerializer_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/ini/IniSerializer_Test.java
new file mode 100644
index 0000000000..cdb2d2156e
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ini/IniSerializer_Test.java
@@ -0,0 +1,221 @@
+/*
+ * 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 FOR A PARTICULAR PURPOSE.  See the
+ * License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ini;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.*;
+import java.util.*;
+
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link IniSerializer}.
+ */
+class IniSerializer_Test {
+
+       
//====================================================================================================
+       // a - Simple bean and flat properties
+       
//====================================================================================================
+
+       @Test
+       void a01_simpleBean() throws Exception {
+               var m = new LinkedHashMap<String, Object>();
+               m.put("host", "localhost");
+               m.put("port", 8080);
+               m.put("debug", true);
+               var ini = IniSerializer.DEFAULT.serialize(m);
+               assertNotNull(ini);
+               assertTrue(ini.contains("host = localhost") || 
ini.contains("host=localhost"));
+               assertTrue(ini.contains("port = 8080") || 
ini.contains("port=8080"));
+               assertTrue(ini.contains("debug = true") || 
ini.contains("debug=true"));
+       }
+
+       @Test
+       void a02_nestedBean() throws Exception {
+               var db = new LinkedHashMap<String, Object>();
+               db.put("host", "localhost");
+               db.put("port", 5432);
+               var config = new LinkedHashMap<String, Object>();
+               config.put("name", "myapp");
+               config.put("database", db);
+               var ini = IniSerializer.DEFAULT.serialize(config);
+               assertNotNull(ini);
+               assertTrue(ini.contains("name") && ini.contains("myapp"));
+               assertTrue(ini.contains("[database]"));
+               assertTrue(ini.contains("host") && ini.contains("localhost"));
+               assertTrue(ini.contains("port") && ini.contains("5432"));
+       }
+
+       @Test
+       void a03_beanWithListOfStrings() throws Exception {
+               var m = new LinkedHashMap<String, Object>();
+               m.put("tags", List.of("web", "api", "rest"));
+               var ini = IniSerializer.DEFAULT.serialize(m);
+               assertNotNull(ini);
+               assertTrue(ini.contains("tags =") || ini.contains("tags="));
+               assertTrue(ini.contains("web") && ini.contains("api") && 
ini.contains("rest"));
+       }
+
+       @Test
+       void a04_nullValues() throws Exception {
+               var m = new LinkedHashMap<String, Object>();
+               m.put("name", "Alice");
+               m.put("middle", null);
+               var s = IniSerializer.create().keepNullProperties().build();
+               var ini = s.serialize(m);
+               assertNotNull(ini);
+               assertTrue(ini.contains("name") && ini.contains("Alice"));
+               assertTrue(ini.contains("null") || ini.contains("middle"));
+       }
+
+       @Test
+       void a05_stringQuoting() throws Exception {
+               var m = new LinkedHashMap<String, Object>();
+               m.put("key", "123");
+               m.put("flag", "true");
+               var ini = IniSerializer.DEFAULT.serialize(m);
+               assertNotNull(ini);
+               assertTrue(ini.contains("'123'") || ini.contains("123"));
+               assertTrue(ini.contains("'true'") || ini.contains("true"));
+       }
+
+       @Test
+       void a06_emptyStrings() throws Exception {
+               var m = new LinkedHashMap<String, Object>();
+               m.put("empty", "");
+               var ini = IniSerializer.DEFAULT.serialize(m);
+               assertNotNull(ini);
+               assertTrue(ini.contains("empty = ''") || 
ini.contains("empty=''") || ini.contains("empty ="));
+       }
+
+       @Test
+       void a07_beanWithMap() throws Exception {
+               var settings = new LinkedHashMap<String, String>();
+               settings.put("timeout", "30");
+               settings.put("retries", "3");
+               var config = new LinkedHashMap<String, Object>();
+               config.put("name", "app");
+               config.put("settings", settings);
+               var ini = IniSerializer.DEFAULT.serialize(config);
+               assertNotNull(ini);
+               assertTrue(ini.contains("[settings]"));
+               assertTrue(ini.contains("timeout") && ini.contains("30"));
+               assertTrue(ini.contains("retries") && ini.contains("3"));
+       }
+
+       @Test
+       void a08_deeplyNestedBean() throws Exception {
+               var company = new LinkedHashMap<String, Object>();
+               company.put("name", "Acme");
+               company.put("ticker", "ACME");
+               var employment = new LinkedHashMap<String, Object>();
+               employment.put("title", "Engineer");
+               employment.put("company", company);
+               var person = new LinkedHashMap<String, Object>();
+               person.put("name", "John");
+               person.put("employment", employment);
+               var ini = IniSerializer.DEFAULT.serialize(person);
+               assertNotNull(ini);
+               assertTrue(ini.contains("name") && ini.contains("John"));
+               assertTrue(ini.contains("employment") && 
(ini.contains("[employment]") || ini.contains("employment")));
+               assertTrue(ini.contains("Acme"));
+       }
+
+       @Test
+       void a09_multipleNestedBeans() throws Exception {
+               var addr1 = new LinkedHashMap<String, Object>();
+               addr1.put("city", "Boston");
+               var addr2 = new LinkedHashMap<String, Object>();
+               addr2.put("city", "NYC");
+               var m = new LinkedHashMap<String, Object>();
+               m.put("name", "x");
+               m.put("home", addr1);
+               m.put("work", addr2);
+               var ini = IniSerializer.DEFAULT.serialize(m);
+               assertNotNull(ini);
+               assertTrue(ini.contains("[home]"));
+               assertTrue(ini.contains("[work]"));
+               assertTrue(ini.contains("Boston") && ini.contains("NYC"));
+       }
+
+       @Test
+       void a10_topLevelCollectionThrows() throws Exception {
+               var list = List.of("a", "b", "c");
+               var ex = assertThrows(Exception.class, () -> 
IniSerializer.DEFAULT.serialize(list));
+               assertTrue(ex.getMessage().contains("Collection") || 
ex.getMessage().contains("not supported")
+                       || ex.getClass().getSimpleName().contains("Serialize"));
+       }
+
+       @Test
+       void a11_topLevelScalarThrows() throws Exception {
+               var ex = assertThrows(Exception.class, () -> 
IniSerializer.DEFAULT.serialize("hello"));
+               assertTrue(ex.getMessage().contains("not supported") || 
ex.getMessage().contains("bean")
+                       || ex.getClass().getSimpleName().contains("Serialize"));
+       }
+
+       @Test
+       void a12_emptyBean() throws Exception {
+               var m = new LinkedHashMap<String, Object>();
+               var ini = IniSerializer.DEFAULT.serialize(m);
+               assertNotNull(ini);
+               assertTrue(ini.trim().isEmpty() || ini.contains("\n\n"));
+       }
+
+       @Test
+       void a13_kvSeparator() throws Exception {
+               var m = new LinkedHashMap<String, Object>();
+               m.put("key", "value");
+               var s = IniSerializer.create().kvSeparator(':').build();
+               var ini = s.serialize(m);
+               assertTrue(ini.contains(":") && ini.contains("value"));
+       }
+
+       @Test
+       void a14_addBeanTypes() throws Exception {
+               var m = new LinkedHashMap<String, Object>();
+               m.put("name", "test");
+               var s = 
IniSerializer.create().addBeanTypes().addRootType().build();
+               var ini = s.serialize(m);
+               assertNotNull(ini);
+               assertTrue(ini.contains("name") && ini.contains("test"));
+       }
+
+       
//====================================================================================================
+       // b - Date/time and enum
+       
//====================================================================================================
+
+       @Test
+       void b01_dateValues() throws Exception {
+               var m = new LinkedHashMap<String, Object>();
+               m.put("date", LocalDate.of(2024, 3, 15));
+               m.put("instant", Instant.parse("2024-03-15T12:00:00Z"));
+               var ini = IniSerializer.DEFAULT.serialize(m);
+               assertNotNull(ini);
+               assertTrue(ini.contains("2024"));
+       }
+
+       @Test
+       void b02_enumValues() throws Exception {
+               var m = new LinkedHashMap<String, Object>();
+               m.put("status", TestEnum.ACTIVE);
+               var ini = IniSerializer.DEFAULT.serialize(m);
+               assertTrue(ini.contains("ACTIVE"));
+       }
+
+       enum TestEnum { ACTIVE, INACTIVE }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/marshaller/Ini_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/marshaller/Ini_Test.java
new file mode 100644
index 0000000000..a6dd9fd388
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/marshaller/Ini_Test.java
@@ -0,0 +1,69 @@
+/*
+ * 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 FOR A PARTICULAR PURPOSE.  See the
+ * License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.marshaller;
+
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+@SuppressWarnings({
+       "unchecked" // Parser returns Object; cast to Map in tests
+})
+class Ini_Test extends TestBase {
+
+       @Test
+       void a01_of() throws Exception {
+               var a = new LinkedHashMap<String, Object>();
+               a.put("name", "test");
+               a.put("count", 42);
+               var ini = Ini.of(a);
+               assertNotNull(ini);
+               assertTrue(ini.contains("name") && ini.contains("test"));
+               assertTrue(ini.contains("count") && ini.contains("42"));
+       }
+
+       @Test
+       void a02_to() throws Exception {
+               var ini = "name = Alice\nage = 30";
+               var m = (Map<String, Object>) Ini.to(ini, Map.class, 
String.class, Object.class);
+               assertBean(m, "name,age", "Alice,30");
+       }
+
+       @Test
+       void a03_roundTrip() throws Exception {
+               var a = new LinkedHashMap<String, Object>();
+               a.put("name", "foo");
+               a.put("value", 123);
+               var ini = Ini.of(a);
+               var b = (Map<String, Object>) Ini.to(ini, Map.class, 
String.class, Object.class);
+               assertBean(b, "name,value", "foo,123");
+       }
+
+       @Test
+       void a04_defaultInstance() throws Exception {
+               var a = new LinkedHashMap<String, Object>();
+               a.put("k", "v");
+               var ini = Ini.DEFAULT.write(a);
+               assertTrue(ini.contains("k") && ini.contains("v"));
+               var b = (Map<String, Object>) Ini.DEFAULT.read(ini, Map.class, 
String.class, Object.class);
+               assertBean(b, "k", "v");
+       }
+}
diff --git a/todo/ini_implementation.md b/todo/1_ini_implementation.md
similarity index 98%
rename from todo/ini_implementation.md
rename to todo/1_ini_implementation.md
index e3f634e2cd..cc4368f121 100644
--- a/todo/ini_implementation.md
+++ b/todo/1_ini_implementation.md
@@ -454,26 +454,13 @@ Path: 
`juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/confi
 
 Add `IniSerializer.class` to `serializers` array and `IniParser.class` to 
`parsers` array.
 
-### 2. `BasicIniConfig.java` (New)
-
-Path: 
`juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicIniConfig.java`
-
-```java
-@Rest(
-    serializers={IniSerializer.class},
-    parsers={IniParser.class},
-    defaultAccept="text/ini"
-)
-public interface BasicIniConfig extends DefaultConfig {}
-```
-
-### 3. `RestClient.java`
+### 2. `RestClient.java`
 
 Path: 
`juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java`
 
 Add `IniSerializer.class` and `IniParser.class` to the `universal()` method's 
serializers/parsers lists.
 
-### 4. Context: `Context.java`
+### 3. Context: `Context.java`
 
 Path: 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java`
 
@@ -973,7 +960,7 @@ the same level of detail.
 13. **Annotation tests** (`IniAnnotation_Test.java`, 
`IniConfigAnnotation_Test.java`)
 14. **Edge case tests** (`IniEdgeCases_Test.java`) -- 13 test cases
 15. **Media type tests** (`IniMediaType_Test.java`)
-16. **REST integration** (`BasicUniversalConfig`, `BasicIniConfig`, 
`RestClient`)
+16. **REST integration** (`BasicUniversalConfig`, `RestClient`)
 17. **Context registration** (`IniConfig`, `IniConfigAnnotation`, 
`Context.java`)
 18. **Final documentation review**
 
diff --git a/todo/hjson_implementation.md b/todo/2_hjson_implementation.md
similarity index 100%
rename from todo/hjson_implementation.md
rename to todo/2_hjson_implementation.md
diff --git a/todo/jcs_implementation.md b/todo/3_jcs_implementation.md
similarity index 100%
rename from todo/jcs_implementation.md
rename to todo/3_jcs_implementation.md
diff --git a/todo/bson_implementation.md b/todo/4_bson_implementation.md
similarity index 100%
rename from todo/bson_implementation.md
rename to todo/4_bson_implementation.md
diff --git a/todo/cbor_implementation.md b/todo/5_cbor_implementation.md
similarity index 100%
rename from todo/cbor_implementation.md
rename to todo/5_cbor_implementation.md
diff --git a/todo/hocon_implementation.md b/todo/6_hocon_implementation.md
similarity index 100%
rename from todo/hocon_implementation.md
rename to todo/6_hocon_implementation.md
diff --git a/todo/parquet_implementation.md b/todo/7_parquet_implementation.md
similarity index 100%
rename from todo/parquet_implementation.md
rename to todo/7_parquet_implementation.md

Reply via email to