Repository: smithy-lang/smithy (2026-04-16T22:00:30Z)

Issue:
Let user extend `SmithyIdlComponentOrder` with custom `Comparator`
Currently, [SmithyIdlComponentOrder](https://github.com/smithy-lang/smithy/blob/84d3ccbd2009e22686e5f8503b1c265dfe0f46ae/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlComponentOrder.java#L19) only takes 3 values:
- `ALPHA_NUMERIC`
- `SOURCE_LOCATION`
- `PREFERRED`

However, users sometimes have valid reasons for wanting their own sort order.

## Our use case: as an example
### Background: a table registry in smithy
We manage database tables via a registry of smithy IDL files (in a git repo). Users sometimes write their IDL from scratch, but usually they start by making a table in a sandbox area of the database. When they are done developing the table, they want to bring it into the registry, so our tooling includes the ability to translate the table into a smithy IDL file on demand.

### The problem: readable smithy IDL
For style and readability reasons, we want to put the table's Struct at the end of the file after all the shapes (column data types, etc) that build up to it. We can't sort the model since it's intrinsically unsorted (a good thing) and sorting the output would be too finicky. If there were an AST between the model and the text output, we could sort that very fluently, but the only options we've found render text directly from the model.

### Workaround: faking source location
Long story short, we're faking source location for each shape to get the desired output. It works, but obviously it's very ugly and wouldn't work for anyone who really needs the source location _and_ control of the component order.

# Proposal: let user bring their own comparator
A `Comparator` might not always be the easiest way to specify a sort. For example, in our case, we mostly just want the table's shape to show up last and there are many easier ways to express that. But any sorting logic can ultimately be specified with a comparator and most logics will be better or best expressed that way anyway.

So, adding this one feature would allow all possible sorts (but IDK how hard it is to add).


Discussion:
We’re closing this issue because it has been open a long time and hasn’t been updated in a while and may not be getting the attention it deserves. We encourage you to check if this is still an issue in the latest release and if you find that this is still a problem, please feel free to comment or open a new issue.
Can we have this triaged? it's still a problem, and we still need a workaround, which isn't great.

by the way - https://nostalebots.xyz/ - just because an issue hasn't been touched, doesn't mean it should be closed. If the issues weren't important, we wouldn't be filing them :)
>but IDK how hard it is to add

It's a bit harder than it sounds at first, since changing from an enum needs to be handled carefully to avoid breaking compatibility.

I'm thinking of updating the serializer to take an interface that effectively codifies the enum, then having the enum implement that. It makes some of the methods public contract, but gives this flexibility for custom use - we also haven't changed this in quite a while so it's probably okay to stabilize. This should open things up safely for the use case here and, since the existing enum methods are package private, maintain binary compatibility.
That makes sense, or add another method that takes an interface and deprecate the old method (while keeping its behavior), if the above can't be done without breaking BC



Patch:
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlComponentOrder.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlComponentOrder.java
index 9c10d947471..1480a7c3edd 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlComponentOrder.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlComponentOrder.java
@@ -10,13 +10,18 @@
 import software.amazon.smithy.model.FromSourceLocation;
 import software.amazon.smithy.model.SourceLocation;
 import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.traits.Trait;
 import software.amazon.smithy.model.traits.TraitDefinition;
 import software.amazon.smithy.utils.MapUtils;
 
 /**
  * Defines how shapes, traits, and metadata are sorted when serializing a model with {@link SmithyIdlModelSerializer}.
+ *
+ * <p>This enum provides the built-in orderings. For custom ordering logic, implement
+ * {@link SmithyIdlSerializationOrder} directly and pass it to
+ * {@link SmithyIdlModelSerializer.Builder#componentOrder(SmithyIdlSerializationOrder)}.
  */
-public enum SmithyIdlComponentOrder {
+public enum SmithyIdlComponentOrder implements SmithyIdlSerializationOrder {
     /**
      * Sort shapes, traits, and metadata alphabetically. Member order, however, is not sorted.
      */
@@ -46,29 +51,36 @@ public enum SmithyIdlComponentOrder {
      */
     PREFERRED;
 
-    Comparator<Shape> shapeComparator() {
+    @Override
+    public Comparator<Shape> shapeComparator() {
         return this == PREFERRED ? new PreferredShapeComparator() : toShapeIdComparator();
     }
 
-    <T extends FromSourceLocation & ToShapeId> Comparator<T> toShapeIdComparator() {
+    @Override
+    public Comparator<Trait> traitComparator() {
+        return toShapeIdComparator();
+    }
+
+    @Override
+    public Comparator<Map.Entry<String, Node>> metadataComparator() {
         switch (this) {
-            case PREFERRED:
             case ALPHA_NUMERIC:
-                return Comparator.comparing(ToShapeId::toShapeId);
+            case PREFERRED:
+                return Map.Entry.comparingByKey();
             case SOURCE_LOCATION:
             default:
-                return new SourceComparator<>();
+                return new MetadataComparator();
         }
     }
 
-    Comparator<Map.Entry<String, Node>> metadataComparator() {
+    private <T extends FromSourceLocation & ToShapeId> Comparator<T> toShapeIdComparator() {
         switch (this) {
-            case ALPHA_NUMERIC:
             case PREFERRED:
-                return Map.Entry.comparingByKey();
+            case ALPHA_NUMERIC:
+                return Comparator.comparing(ToShapeId::toShapeId);
             case SOURCE_LOCATION:
             default:
-                return new MetadataComparator();
+                return new SourceComparator<>();
         }
     }
 
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java
index 1bc176fc029..2f902531185 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java
@@ -60,7 +60,7 @@ public final class SmithyIdlModelSerializer {
     private final Predicate<Trait> traitFilter;
     private final Function<Shape, Path> shapePlacer;
     private final Path basePath;
-    private final SmithyIdlComponentOrder componentOrder;
+    private final SmithyIdlSerializationOrder componentOrder;
     private final String inlineInputSuffix;
     private final String inlineOutputSuffix;
     private final boolean inferInlineIoSuffixes;
@@ -382,7 +382,7 @@ public static final class Builder implements SmithyBuilder<SmithyIdlModelSeriali
         private Function<Shape, Path> shapePlacer = SmithyIdlModelSerializer::placeShapesByNamespace;
         private Path basePath = null;
         private boolean serializePrelude = false;
-        private SmithyIdlComponentOrder componentOrder = SmithyIdlComponentOrder.PREFERRED;
+        private SmithyIdlSerializationOrder componentOrder = SmithyIdlComponentOrder.PREFERRED;
         private String inlineInputSuffix = DEFAULT_INLINE_INPUT_SUFFIX;
         private String inlineOutputSuffix = DEFAULT_INLINE_OUTPUT_SUFFIX;
         private boolean inferInlineIoSuffixes = false;
@@ -477,6 +477,21 @@ public Builder componentOrder(SmithyIdlComponentOrder componentOrder) {
             return this;
         }
 
+        /**
+         * Defines how components are sorted in the model using a custom ordering configuration.
+         *
+         * <p>Implement {@link SmithyIdlSerializationOrder} to provide custom comparators for shapes,
+         * traits, and metadata. The built-in orderings are available as constants on
+         * {@link SmithyIdlComponentOrder}.
+         *
+         * @param componentOrder Custom component ordering configuration.
+         * @return Returns the builder.
+         */
+        public Builder componentOrder(SmithyIdlSerializationOrder componentOrder) {
+            this.componentOrder = Objects.requireNonNull(componentOrder);
+            return this;
+        }
+
         /**
          * Defines what suffixes are checked on operation input shapes to determine whether
          * inline syntax should be used.
@@ -556,7 +571,7 @@ private static final class ShapeSerializer extends ShapeVisitor.Default<Void> {
         private final Predicate<Trait> traitFilter;
         private final Model model;
         private final Set<ShapeId> inlineableShapes;
-        private final SmithyIdlComponentOrder componentOrder;
+        private final SmithyIdlSerializationOrder componentOrder;
 
         ShapeSerializer(
                 SmithyCodeWriter codeWriter,
@@ -564,7 +579,7 @@ private static final class ShapeSerializer extends ShapeVisitor.Default<Void> {
                 Predicate<Trait> traitFilter,
                 Model model,
                 Set<ShapeId> inlineableShapes,
-                SmithyIdlComponentOrder componentOrder
+                SmithyIdlSerializationOrder componentOrder
         ) {
             this.codeWriter = codeWriter;
             this.nodeSerializer = nodeSerializer;
@@ -703,7 +718,7 @@ private void serializeTraits(Map<ShapeId, Trait> traits, TraitFeature... traitFe
                 }
             }
 
-            Comparator<Trait> traitComparator = componentOrder.toShapeIdComparator();
+            Comparator<Trait> traitComparator = componentOrder.traitComparator();
 
             traits.values()
                     .stream()
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlSerializationOrder.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlSerializationOrder.java
new file mode 100644
index 00000000000..aca1b7cb1ca
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlSerializationOrder.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package software.amazon.smithy.model.shapes;
+
+import java.util.Comparator;
+import java.util.Map;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.traits.Trait;
+
+/**
+ * Configures how shapes, traits, and metadata are ordered when serializing a model
+ * with {@link SmithyIdlModelSerializer}.
+ *
+ * <p>The built-in orderings are available as constants on {@link SmithyIdlComponentOrder}.
+ * Implement this interface directly to provide custom ordering logic.
+ *
+ * <p>Only {@link #shapeComparator()} is required. The default implementations of
+ * {@link #traitComparator()} and {@link #metadataComparator()} sort alphabetically,
+ * which is suitable for most use cases.
+ *
+ * @see SmithyIdlComponentOrder
+ * @see SmithyIdlModelSerializer.Builder#componentOrder(SmithyIdlSerializationOrder)
+ */
+public interface SmithyIdlSerializationOrder {
+
+    /**
+     * A shape comparator that sorts alphabetically by shape ID.
+     */
+    Comparator<Shape> ALPHABETICAL = Comparator.comparing(Shape::toShapeId);
+
+    /**
+     * Returns a comparator used to sort shapes within a namespace file.
+     *
+     * @return Comparator for ordering shapes.
+     */
+    Comparator<Shape> shapeComparator();
+
+    /**
+     * Returns a comparator used to sort traits applied to a shape.
+     *
+     * <p>The default implementation sorts traits alphabetically by their shape ID. Custom
+     * implementations that use source-location-based ordering should consider overriding
+     * this method to also sort traits by source location for consistency.
+     *
+     * @return Comparator for ordering traits.
+     */
+    default Comparator<Trait> traitComparator() {
+        return Comparator.comparing(Trait::toShapeId);
+    }
+
+    /**
+     * Returns a comparator used to sort metadata entries.
+     *
+     * <p>The default implementation sorts metadata alphabetically by key.
+     *
+     * @return Comparator for ordering metadata entries.
+     */
+    default Comparator<Map.Entry<String, Node>> metadataComparator() {
+        return Map.Entry.comparingByKey();
+    }
+}


Test patch:
diff --git a/.changes/next-release/feature-d774f63260bae2ee8d11ff1898618d4096b6daac.json b/.changes/next-release/feature-d774f63260bae2ee8d11ff1898618d4096b6daac.json
new file mode 100644
index 00000000000..321ca72ad66
--- /dev/null
+++ b/.changes/next-release/feature-d774f63260bae2ee8d11ff1898618d4096b6daac.json
@@ -0,0 +1,7 @@
+{
+  "type": "feature",
+  "description": "Add support for custom comparators for sorting IDL serialization output",
+  "pull_requests": [
+    "[#3058](https://github.com/smithy-lang/smithy/pull/3058)"
+  ]
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java
index 3826e1ad0c8..8a0de2b1cb5 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java
@@ -22,6 +22,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Comparator;
 import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Stream;
@@ -37,6 +38,8 @@
 import software.amazon.smithy.model.traits.DocumentationTrait;
 import software.amazon.smithy.model.traits.DynamicTrait;
 import software.amazon.smithy.model.traits.RequiredTrait;
+import software.amazon.smithy.model.traits.SensitiveTrait;
+import software.amazon.smithy.model.traits.Trait;
 import software.amazon.smithy.model.traits.synthetic.OriginalShapeIdTrait;
 import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.MapUtils;
@@ -392,4 +395,108 @@ public void unresolvableIdRefSerializedAsQuotedString() {
         Model model2 = Model.assembler().addUnparsedModel("test.smithy", modelResult).assemble().unwrap();
         assertThat(model2, equalTo(model));
     }
+
+    @Test
+    public void usesCustomComponentOrderConfig() {
+        // Build a simple model with multiple shapes to verify custom ordering.
+        Model model = Model.builder()
+                .addShape(StringShape.builder().id("com.example#Zebra").build())
+                .addShape(StringShape.builder().id("com.example#Alpha").build())
+                .addShape(StructureShape.builder().id("com.example#Middle").build())
+                .build();
+
+        // Custom config that reverses the default alphabetical shape order.
+        SmithyIdlSerializationOrder reverseOrder = new SmithyIdlSerializationOrder() {
+            @Override
+            public Comparator<Shape> shapeComparator() {
+                return Comparator.comparing(Shape::toShapeId).reversed();
+            }
+        };
+
+        Map<Path, String> serialized = SmithyIdlModelSerializer.builder()
+                .componentOrder(reverseOrder)
+                .build()
+                .serialize(model);
+        String result = serialized.values().iterator().next();
+
+        // Verify shapes appear in reverse alphabetical order: Zebra, Middle, Alpha.
+        int zebraPos = result.indexOf("Zebra");
+        int middlePos = result.indexOf("Middle");
+        int alphaPos = result.indexOf("Alpha");
+        assertTrue(zebraPos < middlePos, "Zebra should appear before Middle in reverse order");
+        assertTrue(middlePos < alphaPos, "Middle should appear before Alpha in reverse order");
+    }
+
+    @Test
+    public void usesCustomTraitComparator() {
+        // Use two annotation traits (not documentation) whose relative order is observable.
+        Model model = Model.builder()
+                .addShape(StructureShape.builder()
+                        .id("com.example#MyStruct")
+                        .addTrait(new RequiredTrait())
+                        .addTrait(new SensitiveTrait())
+                        .build())
+                .build();
+
+        // Reversed trait comparator: sensitive (s) should appear before required (r) alphabetically,
+        // but reversed means required comes first.
+        SmithyIdlSerializationOrder customOrder = new SmithyIdlSerializationOrder() {
+            @Override
+            public Comparator<Shape> shapeComparator() {
+                return SmithyIdlSerializationOrder.ALPHABETICAL;
+            }
+
+            @Override
+            public Comparator<Trait> traitComparator() {
+                return Comparator.comparing(Trait::toShapeId).reversed();
+            }
+        };
+
+        Map<Path, String> serialized = SmithyIdlModelSerializer.builder()
+                .componentOrder(customOrder)
+                .build()
+                .serialize(model);
+        String result = serialized.get(Paths.get("com.example.smithy"));
+
+        // In reversed order, sensitive (s) comes before required (r).
+        int sensitivePos = result.indexOf("@sensitive");
+        int requiredPos = result.indexOf("@required");
+        assertTrue(sensitivePos < requiredPos,
+                "@sensitive should appear before @required in reversed trait order");
+    }
+
+    @Test
+    public void usesCustomMetadataComparator() {
+        // Use two metadata keys whose relative order is observable.
+        Model model = Model.builder()
+                .addShape(StringShape.builder().id("com.example#Placeholder").build())
+                .putMetadataProperty("zeta", Node.from("z"))
+                .putMetadataProperty("alpha", Node.from("a"))
+                .build();
+
+        // Reversed metadata comparator: alpha (a) should appear before zeta (z) alphabetically,
+        // but reversed means zeta comes first.
+        SmithyIdlSerializationOrder customOrder = new SmithyIdlSerializationOrder() {
+            @Override
+            public Comparator<Shape> shapeComparator() {
+                return SmithyIdlSerializationOrder.ALPHABETICAL;
+            }
+
+            @Override
+            public Comparator<Map.Entry<String, Node>> metadataComparator() {
+                return Map.Entry.<String, Node>comparingByKey().reversed();
+            }
+        };
+
+        Map<Path, String> serialized = SmithyIdlModelSerializer.builder()
+                .componentOrder(customOrder)
+                .build()
+                .serialize(model);
+        String result = serialized.get(Paths.get("metadata.smithy"));
+
+        // In reversed order, zeta (z) comes before alpha (a).
+        int zetaPos = result.indexOf("zeta");
+        int alphaPos = result.indexOf("alpha");
+        assertTrue(zetaPos < alphaPos, "zeta should appear before alpha in reversed metadata order");
+    }
 }