Repository: smithy-lang/smithy (2026-04-08T06:47:33Z)

Issue:
`SmithyIdlSerializer` always renders idRefs unquoted
When serializing a model with an `@idRef` String shape, the `SmithyIdlSerializer` will always render the ShapeId unquoted.

This causes problems when loading the serialized model, because the shape may or may not exist (if `failWhenMissing` isn't used, this isn't a problem beforehand).

For example:

```smithy
$version: "2"

namespace test

@trait structure myTrait { @idRef @required s: String }

@myTrait(s: "foo.bar#Baz") string Demo
```

Becomes:

```smithy
namespace test

use foo.bar#Baz

@trait
structure myTrait {
    @idRef
    @required
    s: String
}

@myTrait(
    s: Baz
)
string Demo
```

which gets rejected when loading:

```
software.amazon.smithy.model.validation.ValidatedResultException: Result contained ERROR severity validation events: 
[DANGER] -: Syntactic shape ID `Baz` does not resolve to a valid shape ID: `foo.bar#Baz`. Did you mean to quote this string? Are you missing a model file? | SyntacticShapeIdTarget test.smithy:15:8
```

Full reproduction in scala-cli:

```scala
//> using scala 3.5.2
//> using dep software.amazon.smithy:smithy-model:1.53.0
//> using option -Wunused:all
import software.amazon.smithy.model.Model
import scala.jdk.CollectionConverters._
import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer

val r = Model
  .assembler()
  .addUnparsedModel(
    "test.smithy",
    """$version: "2"
      |namespace test
      |
      |@trait structure myTrait { @idRef @required s: String }
      |
      |@myTrait(s: "foo.bar#Baz") string Demo
      |""".stripMargin,
  )
  .assemble()

r.getValidationEvents().asScala.toList

val m = r.unwrap()

val srcAgain = SmithyIdlModelSerializer.builder().build().serialize(m).asScala.toList.head._2

Model
  .assembler()
  .addUnparsedModel(
    "test.smithy",
    srcAgain,
  )
  .assemble()
  .unwrap()
```


Discussion:

Patch:
diff --git a/.changes/next-release/feature-23e302f04d1620676aa4ecd7200e110dfe3631bd.json b/.changes/next-release/feature-23e302f04d1620676aa4ecd7200e110dfe3631bd.json
new file mode 100644
index 00000000000..d18cb4ee452
--- /dev/null
+++ b/.changes/next-release/feature-23e302f04d1620676aa4ecd7200e110dfe3631bd.json
@@ -0,0 +1,7 @@
+{
+  "type": "feature",
+  "description": "Quote unresolvable idRef values in IDL serializer",
+  "pull_requests": [
+    "[#3038](https://github.com/smithy-lang/smithy/pull/3038)"
+  ]
+}
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 6773c71c674..1bc176fc029 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
@@ -950,8 +950,10 @@ private void serialize(Node node) {
          */
         private void serialize(Node node, Shape shape) {
             // ShapeIds are represented differently than strings, so if a shape looks like it's
-            // representing a shapeId we need to serialize it without quotes.
-            if (isShapeId(shape)) {
+            // representing a shapeId we need to serialize it without quotes. However, if the
+            // shape ID doesn't resolve to a shape in the model, it must be quoted to ensure
+            // valid IDL output (the target may not exist when @idRef failWhenMissing is false).
+            if (isShapeId(shape) && isResolvableShapeId(node)) {
                 serializeShapeId(node.expectStringNode());
                 return;
             }
@@ -982,6 +984,18 @@ private boolean isShapeId(Shape shape) {
             return shape.getMemberTrait(model, IdRefTrait.class).isPresent();
         }
 
+        private boolean isResolvableShapeId(Node node) {
+            if (!node.isStringNode()) {
+                return false;
+            }
+            try {
+                ShapeId id = ShapeId.from(node.expectStringNode().getValue());
+                return model.getShape(id).isPresent();
+            } catch (ShapeIdSyntaxException e) {
+                return false;
+            }
+        }
+
         private void serializeString(StringNode node) {
             codeWriter.writeInline("$S", node.getValue());
         }


Test patch:
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 70ebb284801..3826e1ad0c8 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
@@ -369,4 +369,27 @@ public void docsNotDuplicateDocsForDynamicDocTrait() {
         assertFalse(modelResult.contains("@documentation"));
         assertTrue(modelResult.contains("/// hello"));
     }
+
+    @Test
+    public void unresolvableIdRefSerializedAsQuotedString() {
+        String stringModel = "$version: \"2.0\"\n"
+                + "namespace ns.foo\n"
+                + "@trait\n"
+                + "structure myTrait {\n"
+                + "    ref: MyRef\n"
+                + "}\n"
+                + "@idRef\n"
+                + "string MyRef\n"
+                + "@myTrait(ref: \"ns.foo#DoesNotExist\")\n"
+                + "string MyTarget\n";
+        Model model = Model.assembler().addUnparsedModel("test.smithy", stringModel).assemble().unwrap();
+        Map<Path, String> reserialized = SmithyIdlModelSerializer.builder().build().serialize(model);
+        String modelResult = reserialized.get(Paths.get("ns.foo.smithy"));
+
+        // Re-assemble the serialized output. On unfixed code, this fails because the
+        // serializer renders the unresolvable idRef as a bare shape ID (DoesNotExist)
+        // which the parser rejects as a syntactic shape ID that doesn't resolve.
+        Model model2 = Model.assembler().addUnparsedModel("test.smithy", modelResult).assemble().unwrap();
+        assertThat(model2, equalTo(model));
+    }
 }