Repository: goadesign/goa (go)

Issue:
Regression: User-Defined Example for Custom URI Type Is Ignored in v1.20

## Description

We define a custom type `utils.URL`, which is a string with `FormatURI`, as follows:

```go
var URL = dsl.Type("URL", dsl.String, func() {
    dsl.Format(dsl.FormatURI)
})
```

When using this type for an attribute and specifying an explicit example, the example is ignored in Goa v1.20 and replaced by the default example (`https://example.com/foo`). This worked as expected in v1.19.

---

## Steps to Reproduce

1. Define the custom type as above.
2. Use it for an attribute with an explicit example:

   ```go
   Attribute("seller_offer_redirect_url", utils.URL, func() {
       Description("URL to redirect to view the offer in the topi tool")
       Example("http://www.seller.topi.eu/offer/739b63c2-9e58-4964-b38d-4ebb424de242%2Fv1")
   })
   ```
3. Generate the OpenAPI/Swagger spec.

---

## Expected Behavior

The provided example URL should appear in the generated OpenAPI spec.

---

## Actual Behavior

The example in the spec is always `https://example.com/foo`, regardless of the explicit example provided in the design.

---

## Regression

* **v1.19:** The explicit example was correctly shown in the generated spec.
* **v1.20:** The explicit example is ignored and replaced by the default.

---

## Screenshot

![Diff Screenshot](https://github.com/user-attachments/assets/4273ea93-f611-415a-8234-ba8b4f6c4636)

---

## Goa Version

* v1.19 (works)
* v1.20 (broken)

---

## Sample Code

```go
var URL = dsl.Type("URL", dsl.String, func() {
    dsl.Format(dsl.FormatURI)
})

Attribute("seller_offer_redirect_url", utils.URL, func() {
    Description("URL to redirect to view the offer in the topi tool")
    Example("http://www.seller.topi.eu/offer/739b63c2-9e58-4964-b38d-4ebb424de242%2Fv1")
})
```

Pull request:
fix(expr): use AttributeExpr for UserType example generation
Previously, UserTypeExpr.recExample() called u.Type.Example() instead of u.AttributeExpr.Example(), which ignored validation rules and custom examples defined on the UserType. This caused UserTypes with format validation (e.g., FormatURI) to generate generic string examples instead of format-appropriate ones, and custom examples were not being used.

The fix ensures that when generating examples for UserTypes, the full AttributeExpr (including validation and examples) is used rather than just the underlying Type.

Fixes #3716

* Add comprehensive tests for UserType example generation
* Test UserTypes with format validation
* Test UserTypes with custom examples
* Test attribute inheritance from UserTypes

Patch:
diff --git a/expr/user_type.go b/expr/user_type.go
index baacbb42..5fbd777c 100644
--- a/expr/user_type.go
+++ b/expr/user_type.go
@@ -96,7 +96,7 @@ func (u *UserTypeExpr) recExample(r *ExampleGenerator) *any {
 	var ex any
 	pex := &ex
 	r.HaveSeen(u.ID(), pex)
-	actual := u.Type.Example(r)
+	actual := u.AttributeExpr.Example(r)
 	*pex = actual
 	return pex
 }


Test patch:
diff --git a/expr/user_type_example_test.go b/expr/user_type_example_test.go
new file mode 100644
index 00000000..b9fc3b54
--- /dev/null
+++ b/expr/user_type_example_test.go
@@ -0,0 +1,176 @@
+package expr_test
+
+import (
+	"testing"
+
+	"goa.design/goa/v3/expr"
+)
+
+// TestUserTypeWithUserExample tests that when an attribute uses a UserType
+// and has a user-defined example, the user example is used.
+func TestUserTypeWithUserExample(t *testing.T) {
+	// Create a UserType "URL" with FormatURI validation
+	urlType := &expr.UserTypeExpr{
+		AttributeExpr: &expr.AttributeExpr{
+			Type: expr.String,
+			Validation: &expr.ValidationExpr{
+				Format: expr.FormatURI,
+			},
+		},
+		TypeName: "URL",
+	}
+
+	// Create an attribute that uses the URL type with a custom example
+	customURL := "http://www.seller.topi.eu/offer/739b63c2-9e58-4964-b38d-4ebb424de242%2Fv1"
+	attr := &expr.AttributeExpr{
+		Type: urlType,
+		UserExamples: []*expr.ExampleExpr{
+			{
+				Summary: "default",
+				Value:   customURL,
+			},
+		},
+	}
+
+	// Test with both randomizers to ensure user examples always take precedence
+	t.Run("with faker randomizer", func(t *testing.T) {
+		exampleGen := expr.NewRandom("test")
+		example := attr.Example(exampleGen)
+		if example != customURL {
+			t.Errorf("Attribute with user example should return %q, got %q", customURL, example)
+		}
+	})
+
+	t.Run("with deterministic randomizer", func(t *testing.T) {
+		gen := expr.NewDeterministicRandomizer()
+		exampleGen := &expr.ExampleGenerator{
+			Randomizer: gen,
+		}
+		example := attr.Example(exampleGen)
+		if example != customURL {
+			t.Errorf("Attribute with user example should return %q, got %q", customURL, example)
+		}
+	})
+}
+
+// TestUserTypeFormatWithCustomExample tests the exact scenario from issue #3716
+// where a UserType with FormatURI has a custom example that should be preserved
+func TestUserTypeFormatWithCustomExample(t *testing.T) {
+	// Create a UserType "URL" with FormatURI validation (no example on the type itself)
+	urlType := &expr.UserTypeExpr{
+		AttributeExpr: &expr.AttributeExpr{
+			Type: expr.String,
+			Validation: &expr.ValidationExpr{
+				Format: expr.FormatURI,
+			},
+		},
+		TypeName: "URL",
+	}
+
+	// Create an attribute using the URL type with a custom example
+	// This simulates: Attribute("seller_offer_redirect_url", URL, func() { Example("...") })
+	customExample := "http://www.seller.topi.eu/offer/739b63c2-9e58-4964-b38d-4ebb424de242%2Fv1"
+	attr := &expr.AttributeExpr{
+		Type: urlType,
+		UserExamples: []*expr.ExampleExpr{
+			{
+				Summary: "default",
+				Value:   customExample,
+			},
+		},
+	}
+
+	// Test with deterministic randomizer (as reported in the issue)
+	gen := expr.NewDeterministicRandomizer()
+	exampleGen := &expr.ExampleGenerator{
+		Randomizer: gen,
+	}
+
+	// The bug was that this would return "https://example.com/foo" instead of the custom example
+	example := attr.Example(exampleGen)
+
+	if example != customExample {
+		t.Errorf("Custom example should be preserved. Expected %q but got %q", customExample, example)
+	}
+}
+
+// TestIssue3716Regression tests the specific regression where UserTypes with
+// format validation would not generate format-appropriate examples
+func TestIssue3716Regression(t *testing.T) {
+	// This test captures what happens when we need to generate an example
+	// for an object that contains a field with a UserType that has format validation
+
+	// Create the URL UserType
+	urlType := &expr.UserTypeExpr{
+		AttributeExpr: &expr.AttributeExpr{
+			Type: expr.String,
+			Validation: &expr.ValidationExpr{
+				Format: expr.FormatURI,
+			},
+		},
+		TypeName: "URL",
+	}
+
+	// Create an object with a field that uses the URL type but no explicit example
+	obj := &expr.Object{
+		{"redirect_url", &expr.AttributeExpr{Type: urlType}},
+	}
+
+	// When generating an example for the object
+	gen := expr.NewDeterministicRandomizer()
+	exampleGen := &expr.ExampleGenerator{
+		Randomizer: gen,
+	}
+
+	example := obj.Example(exampleGen)
+	objExample, ok := example.(map[string]any)
+	if !ok {
+		t.Fatalf("Expected map example, got %T", example)
+	}
+
+	// The redirect_url field should have a proper URI example
+	redirectURL, ok := objExample["redirect_url"].(string)
+	if !ok {
+		t.Fatalf("Expected string for redirect_url, got %T", objExample["redirect_url"])
+	}
+
+	// With the bug, this would be "abc123" (from String type)
+	// With the fix, this should be "https://example.com/foo" (from FormatURI)
+	if redirectURL != "https://example.com/foo" {
+		t.Errorf("Expected URI example for UserType with FormatURI, got %q", redirectURL)
+	}
+}
+
+// TestUserTypeWithOwnExample tests that a UserType can have its own example
+func TestUserTypeWithOwnExample(t *testing.T) {
+	// Create a UserType "URL" with FormatURI validation AND its own example
+	customExample := "https://api.example.com/v1/resources"
+	urlType := &expr.UserTypeExpr{
+		AttributeExpr: &expr.AttributeExpr{
+			Type: expr.String,
+			Validation: &expr.ValidationExpr{
+				Format: expr.FormatURI,
+			},
+			UserExamples: []*expr.ExampleExpr{
+				{
+					Summary: "default",
+					Value:   customExample,
+				},
+			},
+		},
+		TypeName: "URL",
+	}
+
+	// Use deterministic randomizer
+	gen := expr.NewDeterministicRandomizer()
+	exampleGen := &expr.ExampleGenerator{
+		Randomizer: gen,
+	}
+
+	// The UserType itself should return its custom example
+	example := urlType.Example(exampleGen)
+
+	if example != customExample {
+		t.Errorf("UserType with custom example should return %q, got %q", customExample, example)
+	}
+}