1/**
2 * Describes problems found while decoding, encoding, or checking data with
3 * schemas.
4 *
5 * An `Issue` records what failed and, for nested data, where the failure
6 * happened. The Schema system uses these values for missing keys, unexpected
7 * keys, invalid types, invalid values, failed filters, failed transformations,
8 * and alternatives that did not match. This module also formats issues.
9 *
10 * @since 4.0.0
11 */
12import * as Arr from "./Array.ts"
13import { format, formatPath, type Formatter as FormatterI } from "./Formatter.ts"
14import * as InternalAnnotations from "./internal/schema/annotations.ts"
15import * as InternalParser from "./internal/schema/parser.ts"
16import { hasProperty } from "./Predicate.ts"
17import type * as Schema from "./Schema.ts"
18import type * as SchemaAST from "./SchemaAST.ts"
19import type { StandardSchemaV1 } from "./StandardSchema.ts"
20
21const TypeId = "~effect/SchemaIssue/Issue"
22
23/**
24 * Returns `true` if the given value is an {@link Issue}.
25 *
26 * **When to use**
27 *
28 * Use when you need to narrow an `unknown` value to `Issue` in error-handling
29 * code, such as distinguishing an `Issue` from other error types in a catch-all
30 * handler.
31 *
32 * **Details**
33 *
34 * - Checks for the internal `TypeId` brand on the value.
35 *
36 * **Example** (Type-guarding an unknown error)
37 *
38 * ```ts import.meta.vitest
39 * import { SchemaIssue } from "effect"
40 *
41 * const issue = new SchemaIssue.MissingKey(undefined)
42 * SchemaIssue.isIssue(issue) // => true
43 * SchemaIssue.isIssue("not an issue") // => false
44 * ```
45 *
46 * @see {@link Issue}
47 *
48 * @category guards
49 * @since 4.0.0
50 */
51export function isIssue(u: unknown): u is Issue {
52 return hasProperty(u, TypeId) && u[TypeId] === TypeId
53}
54
55/**
56 * Returns `true` when an issue contains an input reported by the schema parser.
57 *
58 * **When to use**
59 *
60 * Use when reading `Issue.input`, especially when `undefined` is a valid input
61 * value.
62 *
63 * **Details**
64 *
65 * Reported input is stored as an own property. This guard checks for that
66 * property and narrows `input` from optional to required.
67 *
68 * **Example** (Reading a reported input)
69 *
70 * ```ts import.meta.vitest
71 * import { Result, Schema, SchemaIssue } from "effect"
72 *
73 * const result = Schema.decodeUnknownResult(Schema.String)(1, { reportInput: true })
74 * if (Result.isFailure(result) && SchemaIssue.hasInput(result.failure.issue)) {
75 * result.failure.issue.input // => 1
76 * }
77 * ```
78 *
79 * @see {@link Issue} for the complete issue model
80 *
81 * @category guards
82 * @since 4.0.0
83 */
84export function hasInput(issue: Issue): issue is Issue & { readonly input: unknown } {
85 return Object.hasOwn(issue, "input")
86}
87
88/**
89 * Union of all terminal (leaf) issue types that have no inner `Issue` children.
90 *
91 * **When to use**
92 *
93 * Use when constraining formatter hooks to only handle terminal nodes or when
94 * pattern matching on the `_tag` of an issue and only leaf nodes matter.
95 *
96 * **Details**
97 *
98 * Members: {@link InvalidType}, {@link InvalidValue}, {@link MissingKey},
99 * {@link UnexpectedKey}, {@link Forbidden}, {@link OneOf}.
100 *
101 * @see {@link Issue} — the full union including composite nodes
102 * @see {@link LeafHook} — formatter hook that operates on `Leaf` values
103 *
104 * @category models
105 * @since 4.0.0
106 */
107export type Leaf =
108 | InvalidType
109 | InvalidValue
110 | MissingKey
111 | UnexpectedKey
112 | Forbidden
113 | OneOf
114
115/**
116 * The root discriminated union of all validation error nodes.
117 *
118 * **When to use**
119 *
120 * Use when typing the error channel in `Effect<A, Issue, R>` results from
121 * schema parsing, or when writing custom formatters or issue-tree walkers.
122 *
123 * **Details**
124 *
125 * Every node has a `_tag` field for pattern-matching. The union includes both
126 * terminal {@link Leaf} types and composite types that wrap inner issues:
127 * {@link Filter}, {@link Encoding}, {@link Pointer}, {@link Composite},
128 * {@link AnyOf}. Use {@link makeFormatterDefault} when a human-readable
129 * representation is needed. When parsing with `reportInput: true`,
130 * value-bearing issues expose the rejected value through an enumerable `input`
131 * field. Built-in formatters may include reported input in default messages. This
132 * is not a general sanitization boundary: paths, ASTs, union successes, and
133 * custom annotations or messages are preserved as supplied and remain the
134 * caller's responsibility.
135 *
136 * @see {@link Leaf} — the terminal subset
137 * @see {@link isIssue} — type guard
138 * @see {@link hasInput} — checks whether an issue reports an input
139 * @category models
140 * @since 4.0.0
141 */
142export type Issue =
143 | Leaf
144 // composite
145 | Filter
146 | Encoding
147 | Pointer
148 | Composite
149 | AnyOf
150
151interface IssueNode {
152 readonly [TypeId]: typeof TypeId
153 /**
154 * The input reported by the schema parser, when input reporting is enabled
155 * and the issue is associated with a present value.
156 */
157 readonly input?: unknown
158}
159
160class IssueNodeImpl implements IssueNode {
161 readonly [TypeId] = TypeId
162 /**
163 * The input reported by the schema parser, when input reporting is enabled
164 * and the issue is associated with a present value.
165 */
166 declare readonly input?: unknown
167 constructor(input?: unknown, options?: SchemaAST.ParseOptions) {
168 if (options?.reportInput === true && input !== InternalParser.missing) {
169 this.input = input
170 }
171 }
172}
173
174/**
175 * Represents a schema issue produced when a schema filter (refinement check) fails.
176 *
177 * **When to use**
178 *
179 * Use when you need to inspect a schema issue that records which refinement
180 * check rejected the value.
181 *
182 * **Details**
183 *
184 * - `filter` is the AST filter node that produced this issue.
185 * - `issue` is the inner issue describing the failure reason.
186 *
187 * **Example** (Matching a Filter issue)
188 *
189 * ```ts import.meta.vitest
190 * import { SchemaAST, SchemaIssue } from "effect"
191 *
192 * const formatIssue = SchemaIssue.makeFormatterDefault()
193 *
194 * function describe(issue: SchemaIssue.Issue): string {
195 * if (issue._tag === "Filter") {
196 * return `Filter failed: ${formatIssue(issue.issue)}`
197 * }
198 * return formatIssue(issue)
199 * }
200 *
201 * const issue = new SchemaIssue.Filter(
202 * SchemaAST.isPattern(/^valid$/),
203 * new SchemaIssue.InvalidValue()
204 * )
205 * describe(issue) // => `Filter failed: Expected a valid value`
206 * ```
207 *
208 * @see {@link Leaf} — terminal issue types that commonly appear as the inner `issue`
209 * @see {@link CheckHook} — formatter hook for `Filter` issues
210 *
211 * @category models
212 * @since 4.0.0
213 */
214export interface Filter extends IssueNode {
215 readonly _tag: "Filter"
216 /**
217 * The filter that failed.
218 */
219 readonly filter: SchemaAST.Filter<unknown>
220 /**
221 * The issue that occurred.
222 */
223 readonly issue: Issue
224}
225
226/**
227 * Constructs a schema issue for a failed refinement check.
228 *
229 * @category constructors
230 * @since 4.0.0
231 */
232export const Filter: new(
233 /**
234 * The filter that failed.
235 */
236 filter: SchemaAST.Filter<any>,
237 /**
238 * The issue that occurred.
239 */
240 issue: Issue,
241 /**
242 * The present input associated with the issue. It is retained only when
243 * `options.reportInput` is `true`.
244 */
245 input?: unknown,
246 /**
247 * The effective parse options controlling input retention.
248 */
249 options?: SchemaAST.ParseOptions
250) => Filter = class extends IssueNodeImpl {
251 readonly _tag = "Filter"
252 /**
253 * The filter that failed.
254 */
255 readonly filter: SchemaAST.Filter<unknown>
256 /**
257 * The issue that occurred.
258 */
259 readonly issue: Issue
260
261 constructor(
262 /**
263 * The filter that failed.
264 */
265 filter: SchemaAST.Filter<any>,
266 /**
267 * The issue that occurred.
268 */
269 issue: Issue,
270 /**
271 * The present input associated with the issue. It is retained only when
272 * `options.reportInput` is `true`.
273 */
274 input?: unknown,
275 /**
276 * The effective parse options controlling input retention.
277 */
278 options?: SchemaAST.ParseOptions
279 ) {
280 super(input, options)
281 this.filter = filter
282 this.issue = issue
283 }
284}
285
286/**
287 * Represents a schema issue produced when a schema transformation (encode/decode step) fails.
288 *
289 * **When to use**
290 *
291 * Use when you need to inspect failures from `Schema.decodeTo` / `Schema.encodeTo`
292 * transformations.
293 *
294 * **Details**
295 *
296 * - `ast` is the AST node for the transformation that failed.
297 * - `issue` is the inner issue describing the failure.
298 *
299 * @see {@link Filter} — failure from a refinement check (not a transformation)
300 * @see {@link Composite} — multiple issues from a single schema node
301 *
302 * @category models
303 * @since 4.0.0
304 */
305export interface Encoding extends IssueNode {
306 readonly _tag: "Encoding"
307 /**
308 * The schema that caused the issue.
309 */
310 readonly ast: SchemaAST.AST
311 /**
312 * The issue that occurred.
313 */
314 readonly issue: Issue
315}
316
317/**
318 * Constructs a schema issue for a failed transformation.
319 *
320 * @category constructors
321 * @since 4.0.0
322 */
323export const Encoding: new(
324 ast: SchemaAST.AST,
325 issue: Issue,
326 input?: unknown,
327 options?: SchemaAST.ParseOptions
328) => Encoding = class extends IssueNodeImpl {
329 readonly _tag = "Encoding"
330 /**
331 * The schema that caused the issue.
332 */
333 readonly ast: SchemaAST.AST
334 /**
335 * The issue that occurred.
336 */
337 readonly issue: Issue
338
339 constructor(
340 /**
341 * The schema that caused the issue.
342 */
343 ast: SchemaAST.AST,
344 /**
345 * The issue that occurred.
346 */
347 issue: Issue,
348 /**
349 * The present input associated with the issue. It is retained only when
350 * `options.reportInput` is `true`.
351 */
352 input?: unknown,
353 /**
354 * The effective parse options controlling input retention.
355 */
356 options?: SchemaAST.ParseOptions
357 ) {
358 super(input, options)
359 this.ast = ast
360 this.issue = issue
361 }
362}
363
364/**
365 * Wraps an inner {@link Issue} with a property-key path, indicating *where* in
366 * a nested structure the error occurred.
367 *
368 * **When to use**
369 *
370 * Use when you need to walk the issue tree to accumulate path segments for error
371 * reporting.
372 *
373 * **Details**
374 *
375 * - `path` is an array of property keys (strings, numbers, or symbols).
376 * - Formatters concatenate nested `Pointer` paths into a single path like
377 * `["a"]["b"][0]`.
378 *
379 * @see {@link Composite} — groups multiple issues under one schema node
380 *
381 * @category models
382 * @since 4.0.0
383 */
384export interface Pointer extends IssueNode {
385 readonly _tag: "Pointer"
386 /**
387 * The path to the location in the input that caused the issue.
388 */
389 readonly path: ReadonlyArray<PropertyKey>
390 /**
391 * The issue that occurred.
392 */
393 readonly issue: Issue
394}
395
396/**
397 * Constructs a schema issue that points to a nested location.
398 *
399 * @category constructors
400 * @since 4.0.0
401 */
402export const Pointer: new(path: ReadonlyArray<PropertyKey>, issue: Issue) => Pointer = class extends IssueNodeImpl {
403 readonly _tag = "Pointer"
404 /**
405 * The path to the location in the input that caused the issue.
406 */
407 readonly path: ReadonlyArray<PropertyKey>
408 /**
409 * The issue that occurred.
410 */
411 readonly issue: Issue
412
413 constructor(
414 /**
415 * The path to the location in the input that caused the issue.
416 */
417 path: ReadonlyArray<PropertyKey>,
418 /**
419 * The issue that occurred.
420 */
421 issue: Issue
422 ) {
423 super()
424 this.path = path
425 this.issue = issue
426 }
427}
428
429/**
430 * Represents a schema issue produced when a required key or tuple index is missing from the input.
431 *
432 * **When to use**
433 *
434 * Use when you need to detect absent fields in struct/tuple validation.
435 *
436 * **Details**
437 *
438 * - `annotations` may contain a custom `messageMissingKey` for formatting.
439 *
440 * @see {@link Pointer} — wraps this issue with the missing key's path
441 * @see {@link UnexpectedKey} — the opposite case (extra key present)
442 *
443 * @category models
444 * @since 4.0.0
445 */
446export interface MissingKey extends IssueNode {
447 readonly _tag: "MissingKey"
448 /**
449 * The metadata for the issue.
450 */
451 readonly annotations: Schema.Annotations.Key<unknown> | undefined
452}
453
454/**
455 * Constructs a schema issue for a missing key or tuple index.
456 *
457 * @category constructors
458 * @since 4.0.0
459 */
460export const MissingKey: new(annotations: Schema.Annotations.Key<unknown> | undefined) => MissingKey = class
461 extends IssueNodeImpl
462{
463 readonly _tag = "MissingKey"
464 /**
465 * The metadata for the issue.
466 */
467 readonly annotations: Schema.Annotations.Key<unknown> | undefined
468
469 constructor(
470 /**
471 * The metadata for the issue.
472 */
473 annotations: Schema.Annotations.Key<unknown> | undefined
474 ) {
475 super()
476 this.annotations = annotations
477 }
478}
479
480/**
481 * Represents a schema issue produced when an input object or tuple contains a key/index not
482 * declared by the schema.
483 *
484 * **When to use**
485 *
486 * Use when you need to detect excess properties during strict struct/tuple
487 * validation.
488 *
489 * **Details**
490 *
491 * - `ast` is the schema that was being validated against.
492 * - `annotations` on `ast` may contain a custom `messageUnexpectedKey`.
493 * - The default formatter renders this as `"Expected no excess property"`, or
494 * `"Unexpected key with value <input>"` when the issue reports an input.
495 *
496 * @see {@link MissingKey} — the opposite case (required key absent)
497 * @see {@link Pointer} — wraps this issue with the unexpected key's path
498 *
499 * @category models
500 * @since 4.0.0
501 */
502export interface UnexpectedKey extends IssueNode {
503 readonly _tag: "UnexpectedKey"
504 /**
505 * The schema that caused the issue.
506 */
507 readonly ast: SchemaAST.AST
508}
509
510/**
511 * Constructs a schema issue for an unexpected key or tuple index.
512 *
513 * @category constructors
514 * @since 4.0.0
515 */
516export const UnexpectedKey: new(
517 ast: SchemaAST.AST,
518 input?: unknown,
519 options?: SchemaAST.ParseOptions
520) => UnexpectedKey = class extends IssueNodeImpl {
521 readonly _tag = "UnexpectedKey"
522 /**
523 * The schema that caused the issue.
524 */
525 readonly ast: SchemaAST.AST
526 constructor(
527 /**
528 * The schema that caused the issue.
529 */
530 ast: SchemaAST.AST,
531 /**
532 * The present input associated with the issue. It is retained only when
533 * `options.reportInput` is `true`.
534 */
535 input?: unknown,
536 /**
537 * The effective parse options controlling input retention.
538 */
539 options?: SchemaAST.ParseOptions
540 ) {
541 super(input, options)
542 this.ast = ast
543 }
544}
545
546/**
547 * Represents a schema issue that groups multiple child issues under a single schema node.
548 *
549 * **When to use**
550 *
551 * Use when you need to walk the issue tree for struct/tuple schemas that collect
552 * all field errors rather than failing on the first.
553 *
554 * **Details**
555 *
556 * - `issues` is a non-empty readonly array (at least one child).
557 * - Formatters flatten `Composite` by recursing into each child.
558 *
559 * @see {@link AnyOf} — used for union no-match errors (similar but different semantics)
560 * @see {@link Pointer} — adds path context to individual issues
561 *
562 * @category models
563 * @since 4.0.0
564 */
565export interface Composite extends IssueNode {
566 readonly _tag: "Composite"
567 /**
568 * The schema that caused the issue.
569 */
570 readonly ast: SchemaAST.AST
571 /**
572 * The issues that occurred.
573 */
574 readonly issues: readonly [Issue, ...Array<Issue>]
575}
576
577/**
578 * Constructs a schema issue that groups multiple child issues.
579 *
580 * @category constructors
581 * @since 4.0.0
582 */
583export const Composite: new(
584 ast: SchemaAST.AST,
585 issues: readonly [Issue, ...Array<Issue>],
586 input?: unknown,
587 options?: SchemaAST.ParseOptions
588) => Composite = class extends IssueNodeImpl {
589 readonly _tag = "Composite"
590 /**
591 * The schema that caused the issue.
592 */
593 readonly ast: SchemaAST.AST
594 /**
595 * The issues that occurred.
596 */
597 readonly issues: readonly [Issue, ...Array<Issue>]
598
599 constructor(
600 /**
601 * The schema that caused the issue.
602 */
603 ast: SchemaAST.AST,
604 /**
605 * The issues that occurred.
606 */
607 issues: readonly [Issue, ...Array<Issue>],
608 /**
609 * The present input associated with the issue. It is retained only when
610 * `options.reportInput` is `true`.
611 */
612 input?: unknown,
613 /**
614 * The effective parse options controlling input retention.
615 */
616 options?: SchemaAST.ParseOptions
617 ) {
618 super(input, options)
619 this.ast = ast
620 this.issues = issues
621 }
622}
623
624/**
625 * Represents a schema issue produced when the runtime type of the input does not match the type
626 * expected by the schema.
627 *
628 * **When to use**
629 *
630 * Use when you need to detect basic type mismatches, such as a wrong primitive
631 * or `null` where an object was expected.
632 *
633 * **Details**
634 *
635 * - `ast` is the schema node that expected a different type.
636 * - The default formatter renders this as `"Expected <type>"`, adding
637 * `", got <input>"` when the issue reports an input.
638 *
639 * **Example** (Formatting a type mismatch)
640 *
641 * ```ts import.meta.vitest
642 * import { Schema, SchemaIssue } from "effect"
643 *
644 * const formatIssue = SchemaIssue.makeFormatterDefault()
645 * const issue = new SchemaIssue.InvalidType(Schema.String.ast)
646 * formatIssue(issue) // => "Expected string"
647 * ```
648 *
649 * @see {@link InvalidValue} — the input has the right type but fails a value constraint
650 *
651 * @category models
652 * @since 4.0.0
653 */
654export interface InvalidType extends IssueNode {
655 readonly _tag: "InvalidType"
656 /**
657 * The schema that caused the issue.
658 */
659 readonly ast: SchemaAST.AST
660}
661
662/**
663 * Constructs a schema issue for an input with an invalid runtime type.
664 *
665 * @category constructors
666 * @since 4.0.0
667 */
668export const InvalidType: new(
669 ast: SchemaAST.AST,
670 input?: unknown,
671 options?: SchemaAST.ParseOptions
672) => InvalidType = class extends IssueNodeImpl {
673 readonly _tag = "InvalidType"
674 /**
675 * The schema that caused the issue.
676 */
677 readonly ast: SchemaAST.AST
678 constructor(
679 /**
680 * The schema that caused the issue.
681 */
682 ast: SchemaAST.AST,
683 /**
684 * The present input associated with the issue. It is retained only when
685 * `options.reportInput` is `true`.
686 */
687 input?: unknown,
688 /**
689 * The effective parse options controlling input retention.
690 */
691 options?: SchemaAST.ParseOptions
692 ) {
693 super(input, options)
694 this.ast = ast
695 }
696}
697
698/**
699 * Represents a schema issue produced when the input has the correct type but its value violates a
700 * constraint (e.g. a string that is too short, a number out of range).
701 *
702 * **When to use**
703 *
704 * Use when you need to detect constraint violations from `Schema.filter`,
705 * `Schema.minLength`, `Schema.greaterThan`, or similar checks.
706 *
707 * **Details**
708 *
709 * - A `message` annotation is returned unchanged and takes precedence over all
710 * other default formatting.
711 * - Without `message`, an `expected` annotation is formatted as
712 * `"Expected <expected>"`, adding `", got <input>"` when input is reported.
713 * - Without either annotation, the default formatter renders
714 * `"Expected a valid value"`, or `"Invalid data <input>"` when input is
715 * reported.
716 *
717 * **Example** (Returning InvalidValue from a custom filter)
718 *
719 * ```ts import.meta.vitest
720 * import { SchemaIssue } from "effect"
721 *
722 * const formatIssue = SchemaIssue.makeFormatterDefault()
723 * const issue = new SchemaIssue.InvalidValue({ message: "must not be empty" })
724 * formatIssue(issue) // => "must not be empty"
725 * ```
726 *
727 * @see {@link InvalidType} — the input has the wrong type entirely
728 * @see {@link Filter} — composite wrapper when a schema filter produces this issue
729 *
730 * @category models
731 * @since 4.0.0
732 */
733export interface InvalidValue extends IssueNode {
734 readonly _tag: "InvalidValue"
735 /**
736 * The metadata for the issue.
737 */
738 readonly annotations: Schema.Annotations.Issue | undefined
739}
740
741/**
742 * Constructs a schema issue for a value that violates a constraint.
743 *
744 * @category constructors
745 * @since 4.0.0
746 */
747export const InvalidValue: new(
748 annotations?: Schema.Annotations.Issue | undefined,
749 input?: unknown,
750 options?: SchemaAST.ParseOptions
751) => InvalidValue = class extends IssueNodeImpl {
752 readonly _tag = "InvalidValue"
753 /**
754 * The metadata for the issue.
755 */
756 readonly annotations: Schema.Annotations.Issue | undefined
757
758 constructor(
759 /**
760 * The metadata for the issue.
761 */
762 annotations?: Schema.Annotations.Issue | undefined,
763 /**
764 * The present input associated with the issue. It is retained only when
765 * `options.reportInput` is `true`.
766 */
767 input?: unknown,
768 /**
769 * The effective parse options controlling input retention.
770 */
771 options?: SchemaAST.ParseOptions
772 ) {
773 super(input, options)
774 this.annotations = annotations
775 }
776}
777
778/** @internal */
779export function makeCompositeAtKey(
780 compositeAst: SchemaAST.AST,
781 pointerKey: PropertyKey,
782 pointerIssue: Issue,
783 compositeInput: unknown,
784 parseOptions?: SchemaAST.ParseOptions
785): Composite {
786 return new Composite(
787 compositeAst,
788 [new Pointer([pointerKey], pointerIssue)],
789 compositeInput,
790 parseOptions
791 )
792}
793
794/**
795 * Represents a schema issue produced when a forbidden operation is encountered during parsing,
796 * such as an asynchronous Effect running inside `Schema.decodeUnknownSync`.
797 *
798 * **When to use**
799 *
800 * Use when you need to detect that a schema requires async execution but was run
801 * synchronously.
802 *
803 * **Details**
804 *
805 * - `annotations` optionally carries a `message` string.
806 * - The default formatter renders this as `"Forbidden operation"`.
807 *
808 * **Example** (Creating a Forbidden issue)
809 *
810 * ```ts import.meta.vitest
811 * import { SchemaIssue } from "effect"
812 *
813 * const formatIssue = SchemaIssue.makeFormatterDefault()
814 * const issue = new SchemaIssue.Forbidden(
815 * { message: "async operation not allowed in sync context" }
816 * )
817 * formatIssue(issue) // => "async operation not allowed in sync context"
818 * ```
819 *
820 * @see {@link InvalidValue} — for value-constraint failures (not operation failures)
821 *
822 * @category models
823 * @since 4.0.0
824 */
825export interface Forbidden extends IssueNode {
826 readonly _tag: "Forbidden"
827 /**
828 * The metadata for the issue.
829 */
830 readonly annotations: Schema.Annotations.Issue | undefined
831}
832
833/**
834 * Constructs a schema issue for a forbidden parsing operation.
835 *
836 * @category constructors
837 * @since 4.0.0
838 */
839export const Forbidden: new(
840 annotations: Schema.Annotations.Issue | undefined,
841 input?: unknown,
842 options?: SchemaAST.ParseOptions
843) => Forbidden = class extends IssueNodeImpl {
844 readonly _tag = "Forbidden"
845 /**
846 * The metadata for the issue.
847 */
848 readonly annotations: Schema.Annotations.Issue | undefined
849
850 constructor(
851 /**
852 * The metadata for the issue.
853 */
854 annotations: Schema.Annotations.Issue | undefined,
855 /**
856 * The present input associated with the issue. It is retained only when
857 * `options.reportInput` is `true`.
858 */
859 input?: unknown,
860 /**
861 * The effective parse options controlling input retention.
862 */
863 options?: SchemaAST.ParseOptions
864 ) {
865 super(input, options)
866 this.annotations = annotations
867 }
868}
869
870/**
871 * Represents a schema issue produced when a value does not match *any* member of a union schema.
872 *
873 * **When to use**
874 *
875 * Use when you need to inspect which union members were attempted and why each
876 * failed.
877 *
878 * **Details**
879 *
880 * - `ast` is the `Union` AST node.
881 * - `issues` contains the per-member failures.
882 *
883 * **Gotchas**
884 *
885 * `issues` is empty when no union member was applicable. In that case, the
886 * default formatter reports the expected type for the union and appends
887 * `", got <input>"` when input is reported.
888 *
889 * @see {@link OneOf} — the opposite: *too many* members matched
890 * @see {@link Composite} — groups multiple issues under a non-union schema
891 *
892 * @category models
893 * @since 4.0.0
894 */
895export interface AnyOf extends IssueNode {
896 readonly _tag: "AnyOf"
897 /**
898 * The schema that caused the issue.
899 */
900 readonly ast: SchemaAST.Union
901 /**
902 * The issues that occurred.
903 */
904 readonly issues: ReadonlyArray<Issue>
905}
906
907/**
908 * Constructs a schema issue for a value that matches no union member.
909 *
910 * @category constructors
911 * @since 4.0.0
912 */
913export const AnyOf: new(
914 ast: SchemaAST.Union,
915 issues: ReadonlyArray<Issue>,
916 input?: unknown,
917 options?: SchemaAST.ParseOptions
918) => AnyOf = class extends IssueNodeImpl {
919 readonly _tag = "AnyOf"
920 /**
921 * The schema that caused the issue.
922 */
923 readonly ast: SchemaAST.Union
924 /**
925 * The issues that occurred.
926 */
927 readonly issues: ReadonlyArray<Issue>
928
929 constructor(
930 /**
931 * The schema that caused the issue.
932 */
933 ast: SchemaAST.Union,
934 /**
935 * The issues that occurred.
936 */
937 issues: ReadonlyArray<Issue>,
938 /**
939 * The present input associated with the issue. It is retained only when
940 * `options.reportInput` is `true`.
941 */
942 input?: unknown,
943 /**
944 * The effective parse options controlling input retention.
945 */
946 options?: SchemaAST.ParseOptions
947 ) {
948 super(input, options)
949 this.ast = ast
950 this.issues = issues
951 }
952}
953
954/**
955 * Represents a schema issue produced when a value matches *multiple* members of a union that is
956 * configured to allow exactly one match (oneOf mode).
957 *
958 * **When to use**
959 *
960 * Use when you need to detect ambiguous union matches when `oneOf` validation is
961 * enabled.
962 *
963 * **Details**
964 *
965 * - `ast` is the `Union` AST node.
966 * - `successes` lists the AST nodes of each member that accepted the input.
967 * - The default formatter renders this as
968 * `"Expected exactly one member to match"`, or
969 * `"Expected exactly one member to match the input <input>"` when input is
970 * reported.
971 *
972 * @see {@link AnyOf} — the opposite: *no* members matched
973 *
974 * @category models
975 * @since 4.0.0
976 */
977export interface OneOf extends IssueNode {
978 readonly _tag: "OneOf"
979 /**
980 * The schema that caused the issue.
981 */
982 readonly ast: SchemaAST.Union
983 /**
984 * The schemas that were successful.
985 */
986 readonly successes: ReadonlyArray<SchemaAST.AST>
987}
988
989/**
990 * Constructs a schema issue for a value that matches multiple union members.
991 *
992 * @category constructors
993 * @since 4.0.0
994 */
995export const OneOf: new(
996 ast: SchemaAST.Union,
997 successes: ReadonlyArray<SchemaAST.AST>,
998 input?: unknown,
999 options?: SchemaAST.ParseOptions
1000) => OneOf = class extends IssueNodeImpl {
1001 readonly _tag = "OneOf"
1002 /**
1003 * The schema that caused the issue.
1004 */
1005 readonly ast: SchemaAST.Union
1006 /**
1007 * The schemas that were successful.
1008 */
1009 readonly successes: ReadonlyArray<SchemaAST.AST>
1010
1011 constructor(
1012 /**
1013 * The schema that caused the issue.
1014 */
1015 ast: SchemaAST.Union,
1016 /**
1017 * The schemas that were successful.
1018 */
1019 successes: ReadonlyArray<SchemaAST.AST>,
1020 /**
1021 * The present input associated with the issue. It is retained only when
1022 * `options.reportInput` is `true`.
1023 */
1024 input?: unknown,
1025 /**
1026 * The effective parse options controlling input retention.
1027 */
1028 options?: SchemaAST.ParseOptions
1029 ) {
1030 super(input, options)
1031 this.ast = ast
1032 this.successes = successes
1033 }
1034}
1035
1036function makeFilterIssue(
1037 entry: Schema.FilterIssue,
1038 input?: unknown,
1039 options?: SchemaAST.ParseOptions
1040): Issue {
1041 if (isIssue(entry)) {
1042 return entry
1043 }
1044 if (typeof entry === "string") {
1045 return new InvalidValue({ message: entry }, input, options)
1046 }
1047 const inner = typeof entry.issue === "string"
1048 ? new InvalidValue({ message: entry.issue }, input, options)
1049 : entry.issue
1050 return new Pointer(entry.path, inner)
1051}
1052
1053/** @internal */
1054export function makeSingle(
1055 out: undefined | boolean | Schema.FilterIssue,
1056 input?: unknown,
1057 options?: SchemaAST.ParseOptions
1058): Issue | undefined {
1059 if (out === undefined) {
1060 return undefined
1061 }
1062 if (typeof out === "boolean") {
1063 return out ? undefined : new InvalidValue(undefined, input, options)
1064 }
1065 return makeFilterIssue(out, input, options)
1066}
1067
1068/** @internal */
1069export function normalizeFilterOutput(
1070 ast: SchemaAST.AST,
1071 out: Schema.FilterOutput,
1072 input?: unknown,
1073 options?: SchemaAST.ParseOptions
1074): Issue | undefined {
1075 if (Array.isArray(out)) {
1076 if (!Arr.isReadonlyArrayNonEmpty(out)) {
1077 return undefined
1078 }
1079 return out.length === 1
1080 ? makeFilterIssue(out[0], input, options)
1081 : new Composite(ast, Arr.map(out, (entry) => makeFilterIssue(entry, input, options)), input, options)
1082 }
1083 return makeSingle(out as undefined | boolean | Schema.FilterIssue, input, options)
1084}
1085
1086/**
1087 * A function type that converts an {@link Issue} into a formatted
1088 * representation. Specialisation of the generic `Formatter` from
1089 * `Formatter.ts` with `Value` fixed to `Issue`.
1090 *
1091 * @see {@link makeFormatterDefault} — creates a `Formatter<string>`
1092 * @see {@link makeFormatterStandardSchemaV1} — creates a `Formatter<StandardSchemaV1.FailureResult>`
1093 *
1094 * @category formatting
1095 * @since 4.0.0
1096 */
1097export interface Formatter<out Format> extends FormatterI<Issue, Format> {}
1098
1099/**
1100 * Callback type used to format {@link Leaf} issues into strings.
1101 *
1102 * **When to use**
1103 *
1104 * Use when customizing how {@link makeFormatterStandardSchemaV1} renders
1105 * terminal issues.
1106 *
1107 * @see {@link defaultLeafHook} — the built-in implementation
1108 * @see {@link Leaf} — the union of terminal issue types
1109 *
1110 * @category formatting
1111 * @since 4.0.0
1112 */
1113export type LeafHook = (issue: Leaf) => string
1114
1115/**
1116 * Returns the built-in {@link LeafHook} used by default formatters.
1117 *
1118 * **When to use**
1119 *
1120 * Use as the default leaf renderer when customizing only the {@link CheckHook}.
1121 *
1122 * **Details**
1123 *
1124 * - Checks for a `message` annotation first; returns it if present.
1125 * - For `InvalidValue`, an `expected` annotation uses the standard expected
1126 * value message and includes reported input when available.
1127 * - Otherwise generates a default message per `_tag`. When the issue reports
1128 * input, the message includes its formatted value where applicable:
1129 * - `InvalidType` → `"Expected <type>"` or `"Expected <type>, got <input>"`
1130 * - `InvalidValue` → `"Expected a valid value"` or `"Invalid data <input>"`
1131 * - `MissingKey` → `"Missing key"`
1132 * - `UnexpectedKey` → `"Expected no excess property"` or
1133 * `"Unexpected key with value <input>"`
1134 * - `Forbidden` → `"Forbidden operation"`
1135 * - `OneOf` → `"Expected exactly one member to match"` or
1136 * `"Expected exactly one member to match the input <input>"`
1137 *
1138 * **Example** (Formatting Standard Schema issues with defaultLeafHook)
1139 *
1140 * ```ts import.meta.vitest
1141 * import { SchemaIssue } from "effect"
1142 *
1143 * const formatter = SchemaIssue.makeFormatterStandardSchemaV1({
1144 * leafHook: SchemaIssue.defaultLeafHook
1145 * })
1146 * formatter(new SchemaIssue.MissingKey(undefined)) // => { issues: [{ path: [], message: "Missing key" }] }
1147 * ```
1148 *
1149 * @see {@link LeafHook}
1150 * @see {@link makeFormatterStandardSchemaV1}
1151 *
1152 * @category formatting
1153 * @since 4.0.0
1154 */
1155export const defaultLeafHook: LeafHook = (issue): string => {
1156 const message = findMessage(issue)
1157 if (message !== undefined) return message
1158 switch (issue._tag) {
1159 case "InvalidType":
1160 return getExpectedMessage(InternalAnnotations.getExpected(issue.ast), issue)
1161 case "InvalidValue": {
1162 const expected = findExpected(issue)
1163 if (expected !== undefined) return getExpectedMessage(expected, issue)
1164 const input = formatInput(issue)
1165 return input === undefined ? "Expected a valid value" : `Invalid data ${input}`
1166 }
1167 case "MissingKey":
1168 return "Missing key"
1169 case "UnexpectedKey": {
1170 const input = formatInput(issue)
1171 return input === undefined ? "Expected no excess property" : `Unexpected key with value ${input}`
1172 }
1173 case "Forbidden":
1174 return "Forbidden operation"
1175 case "OneOf": {
1176 const input = formatInput(issue)
1177 return input === undefined
1178 ? "Expected exactly one member to match"
1179 : `Expected exactly one member to match the input ${input}`
1180 }
1181 }
1182}
1183
1184/**
1185 * Callback type used to format {@link Filter} issues into strings.
1186 *
1187 * **When to use**
1188 *
1189 * Use when customizing how {@link makeFormatterStandardSchemaV1} renders
1190 * filter failures.
1191 *
1192 * **Details**
1193 *
1194 * - Returns `string` to override the message, or `undefined` to fall back to
1195 * the default formatting.
1196 *
1197 * @see {@link defaultCheckHook} — the built-in implementation
1198 * @see {@link Filter} — the issue type this hook formats
1199 *
1200 * @category formatting
1201 * @since 4.0.0
1202 */
1203export type CheckHook = (issue: Filter) => string | undefined
1204
1205/**
1206 * Returns the built-in {@link CheckHook} used by default formatters.
1207 *
1208 * **When to use**
1209 *
1210 * Use as the default filter renderer when customizing only the {@link LeafHook}.
1211 *
1212 * **Details**
1213 *
1214 * - Looks for a `message` annotation on the inner issue first, then on the
1215 * filter itself.
1216 * - Returns `undefined` when no annotation is found, causing the formatter to
1217 * fall back to `"Expected <filter>"` or, when the filter reports input,
1218 * `"Expected <filter>, got <input>"`.
1219 *
1220 * @see {@link CheckHook}
1221 * @see {@link makeFormatterStandardSchemaV1}
1222 *
1223 * @category formatting
1224 * @since 4.0.0
1225 */
1226export const defaultCheckHook: CheckHook = (issue): string | undefined => findMessage(issue.issue) ?? findMessage(issue)
1227
1228/**
1229 * Creates a {@link Formatter} that produces a `StandardSchemaV1.FailureResult`.
1230 *
1231 * **When to use**
1232 *
1233 * Use when you need schema parse errors in
1234 * [Standard Schema V1](https://github.com/standard-schema/standard-schema)
1235 * format, optionally customizing leaf or check issue rendering.
1236 *
1237 * **Details**
1238 *
1239 * - Returns a `Formatter<StandardSchemaV1.FailureResult>`.
1240 * - Each leaf issue is flattened into `{ message, path }` entries.
1241 * - `Pointer` paths are accumulated to produce full property paths.
1242 * - Falls back to {@link defaultLeafHook} / {@link defaultCheckHook} when no
1243 * hooks are provided.
1244 * - Default messages include reported input when the issue that produces the
1245 * message has an `input` field. The returned Standard Schema issues do not
1246 * receive an `input` field.
1247 *
1248 * **Gotchas**
1249 *
1250 * Reported input can appear inside the Standard Schema `message` string even
1251 * though it is not exposed as a separate property. Custom hooks control their
1252 * complete message and are not modified.
1253 *
1254 * **Example** (Creating a Standard Schema V1 formatter)
1255 *
1256 * ```ts import.meta.vitest
1257 * import { SchemaIssue } from "effect"
1258 *
1259 * const formatter = SchemaIssue.makeFormatterStandardSchemaV1()
1260 * formatter(new SchemaIssue.MissingKey(undefined)).issues[0].message // => "Missing key"
1261 * ```
1262 *
1263 * @see {@link makeFormatterDefault} — produces a plain string instead
1264 * @see {@link LeafHook}
1265 * @see {@link CheckHook}
1266 *
1267 * @category formatting
1268 * @since 4.0.0
1269 */
1270export function makeFormatterStandardSchemaV1(options?: {
1271 readonly leafHook?: LeafHook | undefined
1272 readonly checkHook?: CheckHook | undefined
1273}): Formatter<StandardSchemaV1.FailureResult> {
1274 return (issue) => ({
1275 issues: toDefaultIssues(issue, [], options?.leafHook ?? defaultLeafHook, options?.checkHook ?? defaultCheckHook)
1276 })
1277}
1278
1279// A subtype of StandardSchemaV1.Issue
1280type DefaultIssue = {
1281 readonly message: string
1282 readonly path: ReadonlyArray<PropertyKey>
1283}
1284
1285function formatInput(issue: Issue): string | undefined {
1286 return hasInput(issue) ? format(issue.input) : undefined
1287}
1288
1289function findExpected(issue: InvalidValue): string | undefined {
1290 const expected = issue.annotations?.expected
1291 return typeof expected === "string" ? expected : undefined
1292}
1293
1294function getExpectedMessage(expected: string, issue: Issue): string {
1295 const input = formatInput(issue)
1296 return input === undefined ? `Expected ${expected}` : `Expected ${expected}, got ${input}`
1297}
1298
1299function toDefaultIssues(
1300 issue: Issue,
1301 path: ReadonlyArray<PropertyKey>,
1302 leafHook: LeafHook,
1303 checkHook: CheckHook
1304): Array<DefaultIssue> {
1305 switch (issue._tag) {
1306 case "Filter": {
1307 const message = checkHook(issue)
1308 if (message !== undefined) {
1309 return [{ path, message }]
1310 }
1311 if (issue.issue._tag !== "InvalidValue") {
1312 return toDefaultIssues(issue.issue, path, leafHook, checkHook)
1313 }
1314 const expected = findExpected(issue.issue)
1315 return [{
1316 path,
1317 message: expected === undefined
1318 ? getExpectedMessage(formatCheck(issue.filter), issue)
1319 : getExpectedMessage(expected, issue.issue)
1320 }]
1321 }
1322 case "Encoding":
1323 return toDefaultIssues(issue.issue, path, leafHook, checkHook)
1324 case "Pointer":
1325 return toDefaultIssues(issue.issue, [...path, ...issue.path], leafHook, checkHook)
1326 case "Composite":
1327 return issue.issues.flatMap((issue) => toDefaultIssues(issue, path, leafHook, checkHook))
1328 case "AnyOf": {
1329 if (issue.issues.length === 0) {
1330 return [{
1331 path,
1332 message: findMessage(issue) ?? getExpectedMessage(InternalAnnotations.getExpected(issue.ast), issue)
1333 }]
1334 }
1335 return issue.issues.flatMap((issue) => toDefaultIssues(issue, path, leafHook, checkHook))
1336 }
1337 default:
1338 return [{ path, message: leafHook(issue) }]
1339 }
1340}
1341
1342function formatCheck<T>(check: SchemaAST.Check<T>): string {
1343 const expected = check.annotations?.expected
1344 if (typeof expected === "string") return expected
1345
1346 switch (check._tag) {
1347 case "Filter":
1348 return "<filter>"
1349 case "FilterGroup":
1350 return check.checks.map((check) => formatCheck(check)).join(" & ")
1351 }
1352}
1353
1354/**
1355 * Creates a {@link Formatter} that converts an {@link Issue} into a
1356 * human-readable multi-line string.
1357 *
1358 * **When to use**
1359 *
1360 * Use when you need to format a `SchemaIssue.Issue` as error messages for
1361 * logging, CLI output, or developer-facing diagnostics.
1362 *
1363 * **Details**
1364 *
1365 * - Flattens the issue tree into `{ message, path }` entries using
1366 * {@link defaultLeafHook} and {@link defaultCheckHook}.
1367 * - Includes reported input in default messages when the node producing the
1368 * message has an `input` field.
1369 * - Each entry is rendered as `"<message>"` or `"<message>\n at <path>"`.
1370 * - Multiple entries are joined with newlines.
1371 *
1372 * **Gotchas**
1373 *
1374 * Formatting an issue can disclose input retained with `reportInput: true`.
1375 * Wrapper inputs are not inherited by child messages, and custom messages are
1376 * returned unchanged.
1377 *
1378 * **Example** (Formatting an issue as a string)
1379 *
1380 * ```ts import.meta.vitest
1381 * import { SchemaIssue } from "effect"
1382 *
1383 * const formatter = SchemaIssue.makeFormatterDefault()
1384 * formatter(new SchemaIssue.MissingKey(undefined)) // => "Missing key"
1385 * ```
1386 *
1387 * @see {@link makeFormatterStandardSchemaV1} — produces Standard Schema V1 format instead
1388 * @see {@link Formatter}
1389 *
1390 * @category formatting
1391 * @since 4.0.0
1392 */
1393export function makeFormatterDefault(): Formatter<string> {
1394 return (issue) => formatIssue(issue, "")
1395}
1396
1397/** @internal */
1398export const defaultFormatter = makeFormatterDefault()
1399
1400function formatIssue(issue: Issue, path: string): string {
1401 let message: string
1402 switch (issue._tag) {
1403 case "Filter": {
1404 const annotated = defaultCheckHook(issue)
1405 if (annotated !== undefined) {
1406 message = annotated
1407 } else {
1408 if (issue.issue._tag !== "InvalidValue") {
1409 return formatIssue(issue.issue, path)
1410 }
1411 const expected = findExpected(issue.issue)
1412 message = expected === undefined
1413 ? getExpectedMessage(formatCheck(issue.filter), issue)
1414 : getExpectedMessage(expected, issue.issue)
1415 }
1416 break
1417 }
1418 case "Encoding":
1419 return formatIssue(issue.issue, path)
1420 case "Pointer":
1421 return formatIssue(issue.issue, path + formatPath(issue.path))
1422 case "Composite":
1423 case "AnyOf": {
1424 if (issue._tag === "Composite" || issue.issues.length > 0) {
1425 return issue.issues.map((issue) => formatIssue(issue, path)).join("\n")
1426 }
1427 message = findMessage(issue) ?? getExpectedMessage(InternalAnnotations.getExpected(issue.ast), issue)
1428 break
1429 }
1430 default:
1431 message = defaultLeafHook(issue)
1432 break
1433 }
1434 return path ? `${message}\n at ${path}` : message
1435}
1436
1437function findMessage(issue: Issue): string | undefined {
1438 if (issue._tag === "Pointer") return
1439 if (issue._tag === "Encoding") return findMessage(issue.issue)
1440 const annotations = issue._tag === "Filter"
1441 ? issue.filter.annotations
1442 : "annotations" in issue
1443 ? issue.annotations
1444 : issue.ast.annotations
1445 const message = annotations?.[
1446 issue._tag === "MissingKey"
1447 ? "messageMissingKey"
1448 : issue._tag === "UnexpectedKey"
1449 ? "messageUnexpectedKey"
1450 : "message"
1451 ]
1452 if (typeof message === "string") return message
1453}