5050import java .util .List ;
5151import java .util .Map ;
5252import java .util .Set ;
53+ import java .util .TreeMap ;
5354import java .util .UUID ;
5455import java .util .concurrent .CompletableFuture ;
5556import java .util .concurrent .locks .ReentrantLock ;
@@ -712,9 +713,10 @@ public <T> BindResult<List<T>> getListResult(final String path, final Class<T> e
712713 /**
713714 * Read the list at {@code path}. When {@code elementType} carries {@code @KeyIndex} AND the stored node is
714715 * an object, it is read as a key-major section (the section key is the id authority); otherwise it is read
715- * as a plain array. Lenient: an unbindable array element is skipped, and an indexed element whose body id
716- * disagrees with its section key is reconciled to the key. {@code issues}, when non-null, collects the
717- * indexed-read reconciliations.
716+ * as an array, with a read-only tolerance for the legacy numeric-keyed object shape (see
717+ * {@link #listElements}). Lenient: an unbindable array element is skipped, and an indexed element whose
718+ * body id disagrees with its section key is reconciled to the key. {@code issues}, when non-null, collects
719+ * the indexed-read reconciliations.
718720 */
719721 private <T > List <T > readList (final String path , final Class <T > elementType , final Codec codec ,
720722 final List <LoadIssue > issues ) {
@@ -736,10 +738,11 @@ private <T> List<T> readList(final String path, final Class<T> elementType, fina
736738 return ElementStringList .fromArray (node , elementType , mapper , compact );
737739 }
738740 final List <T > out = new ArrayList <>();
739- if (!(node instanceof ArrayNode )) {
741+ final List <JsonNode > elements = listElements (node );
742+ if (elements == null ) {
740743 return out ;
741744 }
742- for (final JsonNode element : node ) {
745+ for (final JsonNode element : elements ) {
743746 try {
744747 out .add (mapper .convertValue (element , elementType ));
745748 } catch (final IllegalArgumentException badElement ) {
@@ -750,6 +753,58 @@ private <T> List<T> readList(final String path, final Class<T> elementType, fina
750753 return out ;
751754 }
752755
756+ /**
757+ * The elements to read as a list from {@code node}: the items of an array, or — for backward
758+ * compatibility with the old FinalConfig storage, which persisted a list as an object keyed by
759+ * throwaway numeric indexes ('0','1','2',...) rather than a real sequence — that object's values in
760+ * index order. Read-only tolerance: {@code save()} always re-emits the modern array form, so a loaded
761+ * legacy list is migrated on the next write. Returns null when {@code node} is neither shape, so the
762+ * caller yields an empty list. The gate is all-numeric keys, which a genuine {@code Map}/POJO never has,
763+ * so nothing but the legacy layout is reinterpreted; {@code @KeyIndex} lists are handled earlier and
764+ * never reach here.
765+ */
766+ private static List <JsonNode > listElements (final JsonNode node ) {
767+ if (node instanceof ArrayNode ) {
768+ final List <JsonNode > items = new ArrayList <>();
769+ for (final JsonNode element : node ) {
770+ items .add (element );
771+ }
772+ return items ;
773+ }
774+ if (node instanceof ObjectNode && node .size () > 0 ) {
775+ final TreeMap <Integer , JsonNode > byIndex = new TreeMap <>();
776+ final Iterator <Map .Entry <String , JsonNode >> it = ((ObjectNode ) node ).fields ();
777+ while (it .hasNext ()) {
778+ final Map .Entry <String , JsonNode > field = it .next ();
779+ final Integer index = asIndexKey (field .getKey ());
780+ if (index == null ) {
781+ return null ; // a non-numeric key means this is not a legacy indexed list
782+ }
783+ byIndex .put (index , field .getValue ());
784+ }
785+ return new ArrayList <>(byIndex .values ());
786+ }
787+ return null ;
788+ }
789+
790+ /** A non-negative decimal index key ('0','1',...) as an int, or null when the key is not one. */
791+ private static Integer asIndexKey (final String key ) {
792+ if (key .isEmpty ()) {
793+ return null ;
794+ }
795+ for (int i = 0 ; i < key .length (); i ++) {
796+ final char ch = key .charAt (i );
797+ if (ch < '0' || ch > '9' ) {
798+ return null ;
799+ }
800+ }
801+ try {
802+ return Integer .valueOf (key );
803+ } catch (final NumberFormatException tooManyDigits ) {
804+ return null ; // an absurdly long digit run is not a real list index
805+ }
806+ }
807+
753808 /**
754809 * Fire nested {@code @PostLoad} for the elements of a list just read (each at {@code path[i]} for a plain
755810 * list, or {@code path.<idValue>} for a {@code @KeyIndex} list), plus each element's descendants — the
0 commit comments