Repository: Effect-TS/effect (TypeScript)

Issue:
From Discord: Custom error message for missing key in schema definition
# Minimal Repro

```ts
import { Schema } from "effect"

const schema = Schema.Struct({
  a: Schema.Array(Schema.String).pipe(
    Schema.minItems(1)
  )
})

Schema.decodeUnknownSync(schema)({}, { errors: "all" })
// throws: TypeError: Cannot read properties of undefined (reading 'length')
```

# Discord thread

https://discord.com/channels/795981131316985866/1308922306470608907



Discussion:

Patch:
diff --git a/.changeset/seven-pianos-sip.md b/.changeset/seven-pianos-sip.md
new file mode 100644
index 00000000000..372a4821d5d
--- /dev/null
+++ b/.changeset/seven-pianos-sip.md
@@ -0,0 +1,36 @@
+---
+"effect": patch
+---
+
+Stable filters such as `minItems`, `maxItems`, and `itemsCount` should be applied only if the from part fails with a `Composite` issue, closes #3980
+
+Before
+
+```ts
+import { Schema } from "effect"
+
+const schema = Schema.Struct({
+  a: Schema.Array(Schema.String).pipe(Schema.minItems(1))
+})
+
+Schema.decodeUnknownSync(schema)({}, { errors: "all" })
+// throws: TypeError: Cannot read properties of undefined (reading 'length')
+```
+
+After
+
+```ts
+import { Schema } from "effect"
+
+const schema = Schema.Struct({
+  a: Schema.Array(Schema.String).pipe(Schema.minItems(1))
+})
+
+Schema.decodeUnknownSync(schema)({}, { errors: "all" })
+/*
+throws:
+ParseError: { readonly a: an array of at least 1 items }
+└─ ["a"]
+   └─ is missing
+*/
+```
diff --git a/packages/effect/src/ParseResult.ts b/packages/effect/src/ParseResult.ts
index a9147d75d67..c053d8655fe 100644
--- a/packages/effect/src/ParseResult.ts
+++ b/packages/effect/src/ParseResult.ts
@@ -806,7 +806,7 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => {
           const result = flatMap(
             orElse(from(i, options), (ef) => {
               const issue = new Refinement(ast, i, "From", ef)
-              if (allErrors && AST.hasStableFilter(ast)) {
+              if (allErrors && AST.hasStableFilter(ast) && isComposite(ef)) {
                 return Option.match(
                   ast.filter(i, options, ast),
                   {


Test patch:
diff --git a/packages/effect/test/Schema/Schema/filter.test.ts b/packages/effect/test/Schema/Schema/filter.test.ts
index 5d3e627e8e8..0d73db7899b 100644
--- a/packages/effect/test/Schema/Schema/filter.test.ts
+++ b/packages/effect/test/Schema/Schema/filter.test.ts
@@ -228,14 +228,15 @@ describe("filter", () => {
     })
   })
 
-  it("stable filters (such as `minItems`, `maxItems`, and `itemsCount`) should generate multiple errors when the 'errors' option is set to 'all'", async () => {
-    const schema = S.Struct({
-      tags: S.Array(S.String.pipe(S.minLength(2))).pipe(S.minItems(3))
-    })
-    await Util.expectDecodeUnknownFailure(
-      schema,
-      { tags: ["AB", "B"] },
-      `{ readonly tags: an array of at least 3 items }
+  describe("Stable Filters (such as `minItems`, `maxItems`, and `itemsCount`)", () => {
+    it("when the 'errors' option is set to 'all', stable filters should generate multiple errors", async () => {
+      const schema = S.Struct({
+        tags: S.Array(S.String.pipe(S.minLength(2))).pipe(S.minItems(3))
+      })
+      await Util.expectDecodeUnknownFailure(
+        schema,
+        { tags: ["AB", "B"] },
+        `{ readonly tags: an array of at least 3 items }
 └─ ["tags"]
    └─ an array of at least 3 items
       ├─ an array of at least 3 items
@@ -248,12 +249,12 @@ describe("filter", () => {
       └─ an array of at least 3 items
          └─ Predicate refinement failure
             └─ Expected an array of at least 3 items, actual ["AB","B"]`,
-      Util.allErrors
-    )
-    await Util.expectDecodeUnknownFailure(
-      schema,
-      { tags: ["AB", "B"] },
-      `{ readonly tags: an array of at least 3 items }
+        Util.allErrors
+      )
+      await Util.expectDecodeUnknownFailure(
+        schema,
+        { tags: ["AB", "B"] },
+        `{ readonly tags: an array of at least 3 items }
 └─ ["tags"]
    └─ an array of at least 3 items
       └─ From side refinement failure
@@ -262,6 +263,30 @@ describe("filter", () => {
                └─ a string at least 2 character(s) long
                   └─ Predicate refinement failure
                      └─ Expected a string at least 2 character(s) long, actual "B"`
-    )
+      )
+    })
+
+    it("when the 'errors' option is set to 'all', stable filters should be applied only if the from part fails with a `Composite` issue", async () => {
+      await Util.expectDecodeUnknownFailure(
+        S.Struct({
+          tags: S.Array(S.String).pipe(S.minItems(1))
+        }),
+        {},
+        `{ readonly tags: an array of at least 1 items }
+└─ ["tags"]
+   └─ is missing`,
+        Util.allErrors
+      )
+      await Util.expectDecodeUnknownFailure(
+        S.Struct({
+          tags: S.Array(S.String).pipe(S.minItems(1), S.maxItems(3))
+        }),
+        {},
+        `{ readonly tags: an array of at most 3 items }
+└─ ["tags"]
+   └─ is missing`,
+        Util.allErrors
+      )
+    })
   })
 })