Skip to content

Fix trait codegen for list with uniqueitems trait#2706

Merged
joewyz merged 3 commits intomainfrom
joewyz/fix-trait-codegen-for-list-of-uniqueitems
Jul 31, 2025
Merged

Fix trait codegen for list with uniqueitems trait#2706
joewyz merged 3 commits intomainfrom
joewyz/fix-trait-codegen-for-list-of-uniqueitems

Conversation

@joewyz
Copy link
Contributor

@joewyz joewyz commented Jul 17, 2025

Background

The current trait codegen is generating incorrect code for list with @uniqueItems. We use Set to represent this kind of list in generated Java code but some part of code still generate List related code.

For example, the following model:

$version: "2.0"

namespace example.traits

@trait
@uniqueItems
list MyListTrait {
    member: MyMemberList
}

@uniqueItems
list MyMemberList {
    member: String
}

will generate incorrect fromNode method because it still use Collection.toList()

public static MyListTrait fromNode(Node node) {
        Builder builder = builder();
        node.expectArrayNode()
            .getElements().stream()
            .map(n -> n.expectArrayNode()
                .getElements().stream()
                .map(n1 -> n1.expectStringNode().getValue())
                .collect(Collectors.toList()))
            .forEach(builder::addValues);
        return builder.build();
}

Similarly, if we have a list of unique items in a structure:

$version: "2.0"

namespace example.traits

@trait
structure MyStruct {
    member: MyMemberList
}

@uniqueItems
list MyMemberList {
    member: String
}

The generated Java code also failed because fromNode did not convert List to Set and getMemberOrEmpty returned emptyList

private final Set<String> member;
public static MyStructTrait fromNode(Node node) {
        Builder builder = builder();
        node.expectObjectNode()
            .expectArrayMember("member", n -> n.expectStringNode().getValue(), builder::member);

        return builder.build();
}

 public Set<String> getMemberOrEmpty() {
        return member == null ? Collections.emptyList() : member;
}

public static final class Builder extends AbstractTraitBuilder<MyStructTrait, Builder> {
        private final BuilderRef<Set<String>> member = BuilderRef.forOrderedSet();

        private Builder() {}

        public Builder member(Set<String> member) {
            clearMember();
            this.member.get().addAll(member);
            return this;
        }
}

This PR fixed the above problems.

Update 7/29/2025

We decided to change the stream style from for loop style to maintain the insertion order of set and map.
The generated code for nested set would be:

@SmithyGenerated
public final class NestedUniqueItemsListTrait extends AbstractTrait implements ToSmithyBuilder<NestedUniqueItemsListTrait> {
    public static final ShapeId ID = ShapeId.from("test.smithy.traitcodegen.lists#NestedUniqueItemsListTrait");

    private final Set<Set<Set<String>>> values;

    private NestedUniqueItemsListTrait(Builder builder) {
        super(ID, builder.getSourceLocation());
        this.values = builder.values.copy();
    }

    @Override
    protected Node createNode() {
        ArrayNode.Builder builder0 = ArrayNode.builder();
        for (Set<Set<String>> element1 : values) {
            ArrayNode.Builder builder1 = ArrayNode.builder();
            for (Set<String> element2 : element1) {
                ArrayNode.Builder builder2 = ArrayNode.builder();
                for (String element3 : element2) {
                    builder2.withValue(Node.from(element3));
                }
                builder1.withValue(builder2.build());
            }
            builder0.withValue(builder1.build());
        }
        return builder0.sourceLocation(getSourceLocation()).build();
    }

    /**
     * Creates a {@link NestedUniqueItemsListTrait} from a {@link Node}.
     *
     * @param node Node to create the NestedUniqueItemsListTrait from.
     * @return Returns the created NestedUniqueItemsListTrait.
     * @throws ExpectationNotMetException if the given Node is invalid.
     */
    public static NestedUniqueItemsListTrait fromNode(Node node) {
        Builder builder = builder();
        List<Node> elements0 = node.expectArrayNode().getElements();
        Set<Set<Set<String>>> value0 = new LinkedHashSet<>();
        for (Node node0 : elements0) {
            List<Node> elements1 = node0.expectArrayNode().getElements();
            Set<Set<String>> value1 = new LinkedHashSet<>();
            for (Node node1 : elements1) {
                List<Node> elements2 = node1.expectArrayNode().getElements();
                Set<String> value2 = new LinkedHashSet<>();
                for (Node node2 : elements2) {
                    String value3 = node2.expectStringNode().getValue();
                    value2.add(value3);
                }
                value1.add(value2);
            }
            value0.add(value1);
        }
        builder.values(value0);
        return builder.build();
    }
    // other code here
}

The generated code for nested map would be:

@SmithyGenerated
public final class NestedMapTrait extends AbstractTrait implements ToSmithyBuilder<NestedMapTrait> {
    public static final ShapeId ID = ShapeId.from("test.smithy.traitcodegen.maps#NestedMapTrait");

    private final Map<String, Map<String, Map<String, String>>> values;

    private NestedMapTrait(Builder builder) {
        super(ID, builder.getSourceLocation());
        this.values = builder.values.copy();
    }

    @Override
    protected Node createNode() {
        ObjectNode.Builder builder0 = ObjectNode.builder();
        for (Entry<String, Map<String, Map<String, String>>> entry1 : values.entrySet()) {
            StringNode key1 = Node.from(entry1.getKey());
            ObjectNode.Builder builder1 = ObjectNode.builder();
            for (Entry<String, Map<String, String>> entry2 : entry1.getValue().entrySet()) {
                StringNode key2 = Node.from(entry2.getKey());
                ObjectNode.Builder builder2 = ObjectNode.builder();
                for (Entry<String, String> entry3 : entry2.getValue().entrySet()) {
                    StringNode key3 = Node.from(entry3.getKey());
                    builder2.withMember(key3, Node.from(entry3.getValue()));
                }
                builder1.withMember(key2, builder2.build());
            }
            builder0.withMember(key1, builder1.build());
        }
        return builder0.sourceLocation(getSourceLocation()).build();
    }

     /**
     * Creates a {@link NestedMapTrait} from a {@link Node}.
     *
     * @param node Node to create the NestedMapTrait from.
     * @return Returns the created NestedMapTrait.
     * @throws ExpectationNotMetException if the given Node is invalid.
     */
    public static NestedMapTrait fromNode(Node node) {
        Builder builder = builder();
        Map<StringNode, Node> members0 = node.expectObjectNode().getMembers();
        for (Entry<StringNode, Node> entry0 : members0.entrySet()) {
            Map<String, Map<String, String>> value1 = new LinkedHashMap<>();
            Map<StringNode, Node> members1 = entry0.getValue().expectObjectNode().getMembers();
            for (Entry<StringNode, Node> entry1 : members1.entrySet()) {
                Map<String, String> value2 = new LinkedHashMap<>();
                Map<StringNode, Node> members2 = entry1.getValue().expectObjectNode().getMembers();
                for (Entry<StringNode, Node> entry2 : members2.entrySet()) {
                    String value3 = entry2.getValue().expectStringNode().getValue();
                    String key3 = entry2.getKey().expectStringNode().getValue();
                    value2.put(key3, value3);
                }
                String key2 = entry1.getKey().expectStringNode().getValue();
                value1.put(key2, value2);
            }
            String key1 = entry0.getKey().expectStringNode().getValue();
            builder.putValues(key1, value1);
        }
        return builder.build();
    }
    // other code here
}

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@joewyz joewyz requested a review from a team as a code owner July 17, 2025 17:35
@joewyz joewyz requested a review from sugmanue July 17, 2025 17:35
@sugmanue
Copy link
Contributor

Let's make sure that we keep ordering of maps and sets of traits. That means to use a LinkedHashSet and LinkedHashMap for Set's and Map's respectively that would preserve the iteration order of how those were read or built.

@joewyz joewyz force-pushed the joewyz/fix-trait-codegen-for-list-of-uniqueitems branch from cb31fad to 145cc45 Compare July 29, 2025 19:38
@joewyz joewyz merged commit aed1ca6 into main Jul 31, 2025
9 checks passed
@joewyz joewyz deleted the joewyz/fix-trait-codegen-for-list-of-uniqueitems branch July 31, 2025 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants