@zio.dev/zio-blocks 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.md +426 -0
- package/package.json +6 -0
- package/path-interpolator.md +645 -0
- package/reference/binding.md +364 -0
- package/reference/chunk.md +576 -0
- package/reference/context.md +157 -0
- package/reference/docs.md +524 -0
- package/reference/dynamic-value.md +823 -0
- package/reference/formats.md +640 -0
- package/reference/json-schema.md +626 -0
- package/reference/json.md +979 -0
- package/reference/modifier.md +276 -0
- package/reference/optics.md +1613 -0
- package/reference/patch.md +631 -0
- package/reference/reflect-transform.md +387 -0
- package/reference/reflect.md +521 -0
- package/reference/registers.md +282 -0
- package/reference/schema-evolution.md +540 -0
- package/reference/schema.md +619 -0
- package/reference/syntax.md +409 -0
- package/reference/type-class-derivation-internals.md +632 -0
- package/reference/typeid.md +900 -0
- package/reference/validation.md +458 -0
- package/scope.md +627 -0
- package/sidebars.js +30 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: validation
|
|
3
|
+
title: "Validation"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`Validation` is a sealed trait in ZIO Blocks that represents declarative constraints on primitive values. Validations are attached to `PrimitiveType` instances and are checked during schema operations like decoding from `DynamicValue` or validating against a `DynamicSchema`.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The validation system in ZIO Blocks provides:
|
|
11
|
+
|
|
12
|
+
- **Declarative constraints** on numeric and string values
|
|
13
|
+
- **Automatic enforcement** during schema-based decoding
|
|
14
|
+
- **Integration with wrapper types** via `transform` for custom validation logic
|
|
15
|
+
- **Schema error reporting** with path information for debugging
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Validation[A]
|
|
19
|
+
├── Validation.None (no constraint)
|
|
20
|
+
│
|
|
21
|
+
├── Validation.Numeric[A] (numeric constraints)
|
|
22
|
+
│ ├── Positive (> 0)
|
|
23
|
+
│ ├── Negative (< 0)
|
|
24
|
+
│ ├── NonPositive (<= 0)
|
|
25
|
+
│ ├── NonNegative (>= 0)
|
|
26
|
+
│ ├── Range[A](min, max) (within bounds)
|
|
27
|
+
│ └── Set[A](values) (one of specific values)
|
|
28
|
+
│
|
|
29
|
+
└── Validation.String (string constraints)
|
|
30
|
+
├── NonEmpty (length > 0)
|
|
31
|
+
├── Empty (length == 0)
|
|
32
|
+
├── Blank (whitespace only)
|
|
33
|
+
├── NonBlank (has non-whitespace)
|
|
34
|
+
├── Length(min, max) (length bounds)
|
|
35
|
+
└── Pattern(regex) (regex match)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Built-in Validations
|
|
39
|
+
|
|
40
|
+
### Numeric Validations
|
|
41
|
+
|
|
42
|
+
Numeric validations apply to `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigInt`, and `BigDecimal`.
|
|
43
|
+
|
|
44
|
+
```scala
|
|
45
|
+
import zio.blocks.schema.Validation
|
|
46
|
+
|
|
47
|
+
// Sign constraints
|
|
48
|
+
Validation.Numeric.Positive // value > 0
|
|
49
|
+
Validation.Numeric.Negative // value < 0
|
|
50
|
+
Validation.Numeric.NonPositive // value <= 0
|
|
51
|
+
Validation.Numeric.NonNegative // value >= 0
|
|
52
|
+
|
|
53
|
+
// Range constraint (inclusive bounds)
|
|
54
|
+
Validation.Numeric.Range(Some(1), Some(100)) // 1 <= value <= 100
|
|
55
|
+
Validation.Numeric.Range(Some(0), None) // value >= 0 (no upper bound)
|
|
56
|
+
Validation.Numeric.Range(None, Some(1000)) // value <= 1000 (no lower bound)
|
|
57
|
+
|
|
58
|
+
// Set constraint (value must be one of the specified values)
|
|
59
|
+
Validation.Numeric.Set(Set(1, 2, 3, 5, 8, 13))
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### String Validations
|
|
63
|
+
|
|
64
|
+
String validations apply to `String` primitive types.
|
|
65
|
+
|
|
66
|
+
```scala
|
|
67
|
+
import zio.blocks.schema.Validation
|
|
68
|
+
|
|
69
|
+
// Content constraints
|
|
70
|
+
Validation.String.NonEmpty // string.nonEmpty (length > 0)
|
|
71
|
+
Validation.String.Empty // string.isEmpty (length == 0)
|
|
72
|
+
Validation.String.Blank // string.trim.isEmpty (whitespace only)
|
|
73
|
+
Validation.String.NonBlank // string.trim.nonEmpty (has non-whitespace)
|
|
74
|
+
|
|
75
|
+
// Length constraint (inclusive bounds)
|
|
76
|
+
Validation.String.Length(Some(1), Some(255)) // 1 <= length <= 255
|
|
77
|
+
Validation.String.Length(Some(3), None) // length >= 3
|
|
78
|
+
Validation.String.Length(None, Some(100)) // length <= 100
|
|
79
|
+
|
|
80
|
+
// Pattern constraint (regex)
|
|
81
|
+
Validation.String.Pattern("^[a-z]+$") // lowercase letters only
|
|
82
|
+
Validation.String.Pattern("^\\d{5}$") // exactly 5 digits
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Validation with PrimitiveType
|
|
86
|
+
|
|
87
|
+
Validations are attached to `PrimitiveType` instances. Each primitive type carries its validation constraint:
|
|
88
|
+
|
|
89
|
+
```scala
|
|
90
|
+
import zio.blocks.schema.{PrimitiveType, Validation}
|
|
91
|
+
|
|
92
|
+
// Int with no validation
|
|
93
|
+
val intType = PrimitiveType.Int(Validation.None)
|
|
94
|
+
|
|
95
|
+
// Positive Int
|
|
96
|
+
val positiveIntType = PrimitiveType.Int(Validation.Numeric.Positive)
|
|
97
|
+
|
|
98
|
+
// Non-empty String
|
|
99
|
+
val nonEmptyStringType = PrimitiveType.String(Validation.String.NonEmpty)
|
|
100
|
+
|
|
101
|
+
// String matching email pattern
|
|
102
|
+
val emailPattern = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"
|
|
103
|
+
val emailStringType = PrimitiveType.String(Validation.String.Pattern(emailPattern))
|
|
104
|
+
|
|
105
|
+
// Int in range 1-10
|
|
106
|
+
val rangeIntType = PrimitiveType.Int(Validation.Numeric.Range(Some(1), Some(10)))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Error Handling with SchemaError
|
|
110
|
+
|
|
111
|
+
When validation fails, ZIO Blocks returns a `SchemaError` that provides detailed information about what went wrong and where.
|
|
112
|
+
|
|
113
|
+
### SchemaError Structure
|
|
114
|
+
|
|
115
|
+
`SchemaError` is an exception that contains one or more error details:
|
|
116
|
+
|
|
117
|
+
```scala
|
|
118
|
+
import zio.blocks.schema.SchemaError
|
|
119
|
+
|
|
120
|
+
// SchemaError wraps a non-empty list of Single errors
|
|
121
|
+
final case class SchemaError(errors: ::[SchemaError.Single]) extends Exception
|
|
122
|
+
|
|
123
|
+
// Single error types
|
|
124
|
+
SchemaError.Single
|
|
125
|
+
├── ConversionFailed(source, details, cause) // transformation/validation failure
|
|
126
|
+
├── MissingField(source, fieldName) // required field not present
|
|
127
|
+
├── DuplicatedField(source, fieldName) // field appears multiple times
|
|
128
|
+
├── ExpectationMismatch(source, expectation) // type mismatch
|
|
129
|
+
├── UnknownCase(source, caseName) // unknown variant case
|
|
130
|
+
└── Message(source, details) // generic message
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Creating Validation Errors
|
|
134
|
+
|
|
135
|
+
Use the factory methods on `SchemaError` to create errors:
|
|
136
|
+
|
|
137
|
+
```scala
|
|
138
|
+
import zio.blocks.schema.SchemaError
|
|
139
|
+
|
|
140
|
+
// For validation failures in transform
|
|
141
|
+
val error = SchemaError.validationFailed("must be positive")
|
|
142
|
+
|
|
143
|
+
// Generic message error
|
|
144
|
+
val msgError = SchemaError("Invalid input")
|
|
145
|
+
|
|
146
|
+
// With path information
|
|
147
|
+
import zio.blocks.schema.DynamicOptic
|
|
148
|
+
val pathError = SchemaError.message("Value out of range", DynamicOptic.root.field("age"))
|
|
149
|
+
|
|
150
|
+
// Conversion failure with details
|
|
151
|
+
val convError = SchemaError.conversionFailed(Nil, "Expected ISO date format")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Error Messages and Paths
|
|
155
|
+
|
|
156
|
+
`SchemaError` includes path information showing where in the data structure the error occurred:
|
|
157
|
+
|
|
158
|
+
```scala
|
|
159
|
+
import zio.blocks.schema.SchemaError
|
|
160
|
+
|
|
161
|
+
val error: SchemaError = ???
|
|
162
|
+
|
|
163
|
+
// Get the full error message
|
|
164
|
+
val msg: String = error.message
|
|
165
|
+
|
|
166
|
+
// Add path context to errors
|
|
167
|
+
val atField = error.atField("name") // prepend .name to path
|
|
168
|
+
val atIndex = error.atIndex(0) // prepend [0] to path
|
|
169
|
+
val atCase = error.atCase("Some") // prepend case context
|
|
170
|
+
|
|
171
|
+
// Combine multiple errors
|
|
172
|
+
val combined = error1 ++ error2
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Example error message:
|
|
176
|
+
```
|
|
177
|
+
Validation failed: value must be positive at: $.user.age
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Integration with Wrapper Types
|
|
181
|
+
|
|
182
|
+
The most common way to use validation in ZIO Blocks is through `transform`, which creates a schema for a wrapper type with validation logic.
|
|
183
|
+
|
|
184
|
+
### Basic Wrapper with Validation
|
|
185
|
+
|
|
186
|
+
```scala
|
|
187
|
+
import zio.blocks.schema.{Schema, SchemaError}
|
|
188
|
+
|
|
189
|
+
case class PositiveInt private (value: Int)
|
|
190
|
+
|
|
191
|
+
object PositiveInt {
|
|
192
|
+
def unsafeMake(n: Int): PositiveInt =
|
|
193
|
+
if (n > 0) PositiveInt(n)
|
|
194
|
+
else throw SchemaError.validationFailed("must be positive")
|
|
195
|
+
|
|
196
|
+
implicit val schema: Schema[PositiveInt] =
|
|
197
|
+
Schema[Int].transform(unsafeMake, _.value)
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Email Type with Regex Validation
|
|
202
|
+
|
|
203
|
+
```scala
|
|
204
|
+
import zio.blocks.schema.{Schema, SchemaError}
|
|
205
|
+
|
|
206
|
+
case class Email private (value: String)
|
|
207
|
+
|
|
208
|
+
object Email {
|
|
209
|
+
private val EmailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$".r
|
|
210
|
+
|
|
211
|
+
def unsafeMake(s: String): Email =
|
|
212
|
+
s match {
|
|
213
|
+
case EmailRegex(_*) => Email(s)
|
|
214
|
+
case _ => throw SchemaError.validationFailed("Invalid email format")
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
implicit val schema: Schema[Email] =
|
|
218
|
+
Schema[String].transform(unsafeMake, _.value).withTypeName[Email]
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### NonEmptyString with Length Validation
|
|
223
|
+
|
|
224
|
+
```scala
|
|
225
|
+
import zio.blocks.schema.{Schema, SchemaError}
|
|
226
|
+
|
|
227
|
+
case class NonEmptyString private (value: String)
|
|
228
|
+
|
|
229
|
+
object NonEmptyString {
|
|
230
|
+
def unsafeMake(s: String): NonEmptyString =
|
|
231
|
+
if (s.nonEmpty) NonEmptyString(s)
|
|
232
|
+
else throw SchemaError.validationFailed("String must not be empty")
|
|
233
|
+
|
|
234
|
+
implicit val schema: Schema[NonEmptyString] =
|
|
235
|
+
Schema[String].transform(unsafeMake, _.value).withTypeName[NonEmptyString]
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Range-Bounded Integer
|
|
240
|
+
|
|
241
|
+
```scala
|
|
242
|
+
import zio.blocks.schema.{Schema, SchemaError}
|
|
243
|
+
|
|
244
|
+
case class Percentage private (value: Int)
|
|
245
|
+
|
|
246
|
+
object Percentage {
|
|
247
|
+
def unsafeMake(n: Int): Percentage =
|
|
248
|
+
if (n >= 0 && n <= 100) Percentage(n)
|
|
249
|
+
else throw SchemaError.validationFailed(s"Percentage must be 0-100, got $n")
|
|
250
|
+
|
|
251
|
+
implicit val schema: Schema[Percentage] =
|
|
252
|
+
Schema[Int].transform(unsafeMake, _.value).withTypeName[Percentage]
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Bidirectional Validation
|
|
257
|
+
|
|
258
|
+
Use the two-argument `transform` for cases where both encoding and decoding need validation:
|
|
259
|
+
|
|
260
|
+
```scala
|
|
261
|
+
import zio.blocks.schema.{Schema, SchemaError}
|
|
262
|
+
|
|
263
|
+
case class BoundedValue(value: Int)
|
|
264
|
+
|
|
265
|
+
object BoundedValue {
|
|
266
|
+
implicit val schema: Schema[BoundedValue] = Schema[Int].transform(
|
|
267
|
+
wrap = n =>
|
|
268
|
+
if (n >= 0 && n < 100) BoundedValue(n)
|
|
269
|
+
else throw SchemaError.validationFailed("Value must be in [0, 100)"),
|
|
270
|
+
unwrap = v =>
|
|
271
|
+
if (v.value >= 0) v.value
|
|
272
|
+
else throw SchemaError.validationFailed("Corrupted value")
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Validation at Encode/Decode Time
|
|
278
|
+
|
|
279
|
+
### Decoding with Validation
|
|
280
|
+
|
|
281
|
+
When decoding from `DynamicValue` or JSON, validations in wrapper schemas are automatically enforced:
|
|
282
|
+
|
|
283
|
+
```scala
|
|
284
|
+
import zio.blocks.schema._
|
|
285
|
+
import zio.blocks.schema.json._
|
|
286
|
+
|
|
287
|
+
case class PositiveInt(value: Int)
|
|
288
|
+
object PositiveInt {
|
|
289
|
+
def unsafeMake(n: Int): PositiveInt =
|
|
290
|
+
if (n > 0) PositiveInt(n)
|
|
291
|
+
else throw SchemaError.validationFailed("must be positive")
|
|
292
|
+
|
|
293
|
+
implicit val schema: Schema[PositiveInt] =
|
|
294
|
+
Schema[Int].transform(unsafeMake, _.value)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case class Order(quantity: PositiveInt, price: BigDecimal)
|
|
298
|
+
object Order {
|
|
299
|
+
implicit val schema: Schema[Order] = Schema.derived
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// JSON decoding will validate PositiveInt
|
|
303
|
+
val json = """{"quantity": -5, "price": 99.99}"""
|
|
304
|
+
val result = JsonDecoder[Order].decodeString(json)
|
|
305
|
+
// result: Left(SchemaError: must be positive at $.quantity)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### DynamicValue Validation
|
|
309
|
+
|
|
310
|
+
Use `DynamicSchema` to validate `DynamicValue` instances:
|
|
311
|
+
|
|
312
|
+
```scala
|
|
313
|
+
import zio.blocks.schema._
|
|
314
|
+
|
|
315
|
+
case class Person(name: String, age: Int)
|
|
316
|
+
object Person {
|
|
317
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Create a DynamicSchema for validation
|
|
321
|
+
val dynamicSchema: DynamicSchema = Schema[Person].toDynamicSchema
|
|
322
|
+
|
|
323
|
+
// Create a DynamicValue to validate
|
|
324
|
+
val value = DynamicValue.Record(Vector(
|
|
325
|
+
"name" -> DynamicValue.Primitive(PrimitiveValue.String("Alice")),
|
|
326
|
+
"age" -> DynamicValue.Primitive(PrimitiveValue.Int(30))
|
|
327
|
+
))
|
|
328
|
+
|
|
329
|
+
// Validate the value
|
|
330
|
+
val checkResult: Option[SchemaError] = dynamicSchema.check(value)
|
|
331
|
+
// None if valid, Some(error) if invalid
|
|
332
|
+
|
|
333
|
+
val isValid: Boolean = dynamicSchema.conforms(value)
|
|
334
|
+
// true if valid
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Converting DynamicSchema to Validating Schema
|
|
338
|
+
|
|
339
|
+
`DynamicSchema.toSchema` creates a `Schema[DynamicValue]` that rejects non-conforming values:
|
|
340
|
+
|
|
341
|
+
```scala
|
|
342
|
+
import zio.blocks.schema._
|
|
343
|
+
|
|
344
|
+
val dynamicSchema: DynamicSchema = Schema[Person].toDynamicSchema
|
|
345
|
+
val validatingSchema: Schema[DynamicValue] = dynamicSchema.toSchema
|
|
346
|
+
|
|
347
|
+
// Now any decoding through this schema will validate structure
|
|
348
|
+
val invalidValue = DynamicValue.Record(Vector(
|
|
349
|
+
"name" -> DynamicValue.Primitive(PrimitiveValue.Int(42)) // wrong type!
|
|
350
|
+
))
|
|
351
|
+
|
|
352
|
+
val result = validatingSchema.fromDynamicValue(invalidValue)
|
|
353
|
+
// Left(SchemaError: Expected String, got Int at $.name)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Validation in JSON Schema
|
|
357
|
+
|
|
358
|
+
When deriving JSON Schema from a ZIO Blocks schema, validations are reflected in the output:
|
|
359
|
+
|
|
360
|
+
```scala
|
|
361
|
+
import zio.blocks.schema._
|
|
362
|
+
import zio.blocks.schema.json.JsonSchema
|
|
363
|
+
|
|
364
|
+
// Numeric validations become JSON Schema constraints
|
|
365
|
+
// Validation.Numeric.Range(Some(0), Some(100)) → "minimum": 0, "maximum": 100
|
|
366
|
+
|
|
367
|
+
// String validations become JSON Schema constraints
|
|
368
|
+
// Validation.String.NonEmpty → "minLength": 1
|
|
369
|
+
// Validation.String.Length(Some(1), Some(255)) → "minLength": 1, "maxLength": 255
|
|
370
|
+
// Validation.String.Pattern("^[a-z]+$") → "pattern": "^[a-z]+$"
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
When parsing JSON Schema, these constraints are converted back to `Validation` instances.
|
|
374
|
+
|
|
375
|
+
## Composing Validations
|
|
376
|
+
|
|
377
|
+
The current `Validation` ADT does not support combining multiple validations on a single primitive (e.g., both `NonEmpty` and `Pattern`). For complex validation logic, use `transform`:
|
|
378
|
+
|
|
379
|
+
```scala
|
|
380
|
+
import zio.blocks.schema.{Schema, SchemaError}
|
|
381
|
+
|
|
382
|
+
case class Username private (value: String)
|
|
383
|
+
|
|
384
|
+
object Username {
|
|
385
|
+
private val UsernameRegex = "^[a-z][a-z0-9_]{2,19}$".r
|
|
386
|
+
|
|
387
|
+
def unsafeMake(s: String): Username = {
|
|
388
|
+
if (s.isEmpty)
|
|
389
|
+
throw SchemaError.validationFailed("Username cannot be empty")
|
|
390
|
+
else if (s.length < 3)
|
|
391
|
+
throw SchemaError.validationFailed("Username must be at least 3 characters")
|
|
392
|
+
else if (s.length > 20)
|
|
393
|
+
throw SchemaError.validationFailed("Username cannot exceed 20 characters")
|
|
394
|
+
else if (!s.matches(UsernameRegex.regex))
|
|
395
|
+
throw SchemaError.validationFailed("Username must start with a letter and contain only lowercase letters, numbers, and underscores")
|
|
396
|
+
else
|
|
397
|
+
Username(s)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
implicit val schema: Schema[Username] =
|
|
401
|
+
Schema[String].transform(unsafeMake, _.value).withTypeName[Username]
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Best Practices
|
|
406
|
+
|
|
407
|
+
### 1. Use Wrapper Types for Domain Validation
|
|
408
|
+
|
|
409
|
+
Prefer creating dedicated wrapper types with `transform` over relying solely on `Validation` constraints:
|
|
410
|
+
|
|
411
|
+
```scala
|
|
412
|
+
// Good: Explicit domain type with validation
|
|
413
|
+
case class OrderId private (value: String)
|
|
414
|
+
object OrderId {
|
|
415
|
+
def unsafeMake(s: String): OrderId =
|
|
416
|
+
if (s.matches("^ORD-\\d{8}$")) OrderId(s)
|
|
417
|
+
else throw SchemaError.validationFailed("Invalid order ID format")
|
|
418
|
+
|
|
419
|
+
implicit val schema: Schema[OrderId] =
|
|
420
|
+
Schema[String].transform(unsafeMake, _.value).withTypeName[OrderId]
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Less ideal: Raw String with separate validation
|
|
424
|
+
val orderIdString: String = ???
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### 2. Provide Clear Error Messages
|
|
428
|
+
|
|
429
|
+
Include context in error messages to help users understand what went wrong:
|
|
430
|
+
|
|
431
|
+
```scala
|
|
432
|
+
// Good: Specific, actionable error message
|
|
433
|
+
throw SchemaError.validationFailed(
|
|
434
|
+
s"Age must be between 0 and 150, got $age"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
// Less helpful: Generic message
|
|
438
|
+
throw SchemaError.validationFailed("Invalid age")
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### 3. Combine Errors When Possible
|
|
442
|
+
|
|
443
|
+
For multiple validation failures, combine them into a single `SchemaError`:
|
|
444
|
+
|
|
445
|
+
```scala
|
|
446
|
+
def validate(input: Input): Either[SchemaError, ValidInput] = {
|
|
447
|
+
val errors = List.newBuilder[SchemaError]
|
|
448
|
+
|
|
449
|
+
if (input.name.isEmpty)
|
|
450
|
+
errors += SchemaError.validationFailed("name is required")
|
|
451
|
+
if (input.age < 0)
|
|
452
|
+
errors += SchemaError.validationFailed("age must be non-negative")
|
|
453
|
+
|
|
454
|
+
val allErrors = errors.result()
|
|
455
|
+
if (allErrors.isEmpty) Right(ValidInput(input))
|
|
456
|
+
else Left(allErrors.reduce(_ ++ _))
|
|
457
|
+
}
|
|
458
|
+
```
|