@@ -661,10 +661,134 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
661661 items .getAdditionalProperties ().dataType
662662 ));
663663 }
664+
665+ // Recursively register builder factories for every nested
666+ // container reachable from this property. Without this step,
667+ // shapes like Map<String, List<X>> (e.g. an OpenAPI object
668+ // with `additionalProperties: { type: array, items: ...}`)
669+ // never get a `BuiltList<X>` factory registered, and
670+ // built_value throws "No builder factory for BuiltList<X>"
671+ // at deserialization time.
672+ registerNestedBuilderFactories (property );
664673 }
665674 }
666675 }
667676
677+ /**
678+ * Walk the CodegenProperty tree and register a built_value
679+ * BuilderFactory for every container node, including the top one.
680+ * Handles arbitrary nesting like {@code Map<String, List<X>>},
681+ * {@code List<Map<String, X>>}, {@code List<List<X>>}, etc.
682+ */
683+ private void registerNestedBuilderFactories (CodegenProperty prop ) {
684+ if (prop == null || !prop .isContainer || prop .items == null ) {
685+ return ;
686+ }
687+ // Recurse first so deeper containers are registered too.
688+ registerNestedBuilderFactories (prop .items );
689+
690+ if (prop .items .isContainer ) {
691+ // Truly nested container (e.g. Map<String, List<X>>):
692+ // must use composite form because the simple constructor
693+ // cannot express the nested FullType.
694+ BuilderFactoryExpr expr = renderBuilderFactory (prop );
695+ if (expr != null ) {
696+ addBuiltValueSerializer (BuiltValueSerializer .composite (
697+ expr .fullTypeArgs ,
698+ expr .builderInstantiation ));
699+ }
700+ } else {
701+ // Leaf container (e.g. List<X>, Map<String, X>): use the
702+ // same simple constructor the rest of the codegen uses so
703+ // the Set deduplicates correctly.
704+ addBuiltValueSerializer (new BuiltValueSerializer (
705+ prop .isArray ,
706+ prop .getUniqueItems (),
707+ prop .isMap ,
708+ prop .items .isNullable ,
709+ prop .items .dataType ));
710+ }
711+ }
712+
713+ /**
714+ * Render the FullType argument list and the matching Builder
715+ * instantiation for a container CodegenProperty.
716+ *
717+ * @return null if {@code prop} is not a container we can render.
718+ */
719+ private BuilderFactoryExpr renderBuilderFactory (CodegenProperty prop ) {
720+ if (prop == null || !prop .isContainer || prop .items == null ) {
721+ return null ;
722+ }
723+ String innerFullType = renderInnerFullType (prop .items );
724+ String innerDart = renderDartType (prop .items );
725+
726+ if (prop .isArray ) {
727+ String collection = prop .getUniqueItems () ? "BuiltSet" : "BuiltList" ;
728+ String builder = prop .getUniqueItems () ? "SetBuilder" : "ListBuilder" ;
729+ return new BuilderFactoryExpr (
730+ collection + ", [FullType(" + innerFullType + ")]" ,
731+ builder + "<" + innerDart + ">" );
732+ }
733+ if (prop .isMap ) {
734+ return new BuilderFactoryExpr (
735+ "BuiltMap, [FullType(String), FullType(" + innerFullType + ")]" ,
736+ "MapBuilder<String, " + innerDart + ">" );
737+ }
738+ return null ;
739+ }
740+
741+ /**
742+ * What goes inside {@code FullType(...)} for this property:
743+ * a leaf type name like {@code "Foo"}, or a nested expression like
744+ * {@code "BuiltList, [FullType(Foo)]"}.
745+ */
746+ private String renderInnerFullType (CodegenProperty prop ) {
747+ if (prop == null ) return "dynamic" ;
748+ if (!prop .isContainer || prop .items == null ) {
749+ return prop .dataType ;
750+ }
751+ String inner = renderInnerFullType (prop .items );
752+ if (prop .isArray ) {
753+ String collection = prop .getUniqueItems () ? "BuiltSet" : "BuiltList" ;
754+ return collection + ", [FullType(" + inner + ")]" ;
755+ }
756+ if (prop .isMap ) {
757+ return "BuiltMap, [FullType(String), FullType(" + inner + ")]" ;
758+ }
759+ return prop .dataType ;
760+ }
761+
762+ /**
763+ * Render the Dart type literal (e.g. {@code BuiltMap<String, BuiltList<Foo>>})
764+ * used inside the {@code () => XBuilder<...>()} lambda.
765+ */
766+ private String renderDartType (CodegenProperty prop ) {
767+ if (prop == null ) return "dynamic" ;
768+ if (!prop .isContainer || prop .items == null ) {
769+ return prop .dataType ;
770+ }
771+ String inner = renderDartType (prop .items );
772+ if (prop .isArray ) {
773+ String collection = prop .getUniqueItems () ? "BuiltSet" : "BuiltList" ;
774+ return collection + "<" + inner + ">" ;
775+ }
776+ if (prop .isMap ) {
777+ return "BuiltMap<String, " + inner + ">" ;
778+ }
779+ return prop .dataType ;
780+ }
781+
782+ private static final class BuilderFactoryExpr {
783+ final String fullTypeArgs ;
784+ final String builderInstantiation ;
785+
786+ BuilderFactoryExpr (String fullTypeArgs , String builderInstantiation ) {
787+ this .fullTypeArgs = fullTypeArgs ;
788+ this .builderInstantiation = builderInstantiation ;
789+ }
790+ }
791+
668792 @ Override
669793 public OperationsMap postProcessOperationsWithModels (OperationsMap objs , List <ModelMap > allModels ) {
670794 super .postProcessOperationsWithModels (objs , allModels );
@@ -833,12 +957,45 @@ static class BuiltValueSerializer {
833957
834958 @ Getter final String dataType ;
835959
960+ // When non-null, the serializer is rendered verbatim from these
961+ // pre-computed strings instead of being dispatched through the
962+ // isArray/isMap branches in serializers.mustache. Used for
963+ // arbitrarily nested container types where dataType alone can't
964+ // express the FullType expression (e.g. Map<String, List<X>>).
965+ @ Getter final String fullTypeArgs ;
966+
967+ @ Getter final String builderInstantiation ;
968+
836969 private BuiltValueSerializer (boolean isArray , boolean uniqueItems , boolean isMap , boolean isNullable , String dataType ) {
837970 this .isArray = isArray ;
838971 this .uniqueItems = uniqueItems ;
839972 this .isMap = isMap ;
840973 this .isNullable = isNullable ;
841974 this .dataType = dataType ;
975+ this .fullTypeArgs = null ;
976+ this .builderInstantiation = null ;
977+ }
978+
979+ private BuiltValueSerializer (String fullTypeArgs , String builderInstantiation ) {
980+ this .isArray = false ;
981+ this .uniqueItems = false ;
982+ this .isMap = false ;
983+ this .isNullable = false ;
984+ this .dataType = "" ;
985+ this .fullTypeArgs = fullTypeArgs ;
986+ this .builderInstantiation = builderInstantiation ;
987+ }
988+
989+ /**
990+ * Build a serializer for a nested-container BuilderFactory whose
991+ * type signature can't be expressed by the simple
992+ * (isArray, isMap, dataType) form. {@code fullTypeArgs} is the
993+ * argument list for {@code FullType(...)} (without the wrapping
994+ * call) and {@code builderInstantiation} is the inside of
995+ * {@code () => ...()}.
996+ */
997+ public static BuiltValueSerializer composite (String fullTypeArgs , String builderInstantiation ) {
998+ return new BuiltValueSerializer (fullTypeArgs , builderInstantiation );
842999 }
8431000
8441001 public boolean isArray () {
@@ -858,11 +1015,18 @@ public boolean equals(Object o) {
8581015 if (this == o ) return true ;
8591016 if (o == null || getClass () != o .getClass ()) return false ;
8601017 BuiltValueSerializer that = (BuiltValueSerializer ) o ;
1018+ if (fullTypeArgs != null || that .fullTypeArgs != null ) {
1019+ return Objects .equals (fullTypeArgs , that .fullTypeArgs )
1020+ && Objects .equals (builderInstantiation , that .builderInstantiation );
1021+ }
8611022 return isArray == that .isArray && uniqueItems == that .uniqueItems && isMap == that .isMap && isNullable == that .isNullable && dataType .equals (that .dataType );
8621023 }
8631024
8641025 @ Override
8651026 public int hashCode () {
1027+ if (fullTypeArgs != null ) {
1028+ return Objects .hash (fullTypeArgs , builderInstantiation );
1029+ }
8661030 return Objects .hash (isArray , uniqueItems , isMap , isNullable , dataType );
8671031 }
8681032 }
0 commit comments