@bedrockio/yada 1.0.0

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/README.md ADDED
@@ -0,0 +1,750 @@
1
+ # Yada
2
+
3
+ Yada is a validation library inspired by [Joi](https://joi.dev/). The name
4
+ derives from a
5
+ [Japanese slang phrase](https://jisho.org/word/%E3%82%84%E3%81%A0) that
6
+ expresses rejection. Its goal is to provide an API for validation that is simple
7
+ and composable.
8
+
9
+ Concepts
10
+
11
+ - [Installation](#installation)
12
+ - [Getting Started](#getting-started)
13
+ - [Types](#types)
14
+ - [String](#string)
15
+ - [Number](#number)
16
+ - [Boolean](#boolean)
17
+ - [Array](#array)
18
+ - [Object](#object)
19
+ - [Date](#date)
20
+ - [Common Methods](#common-methods)
21
+ - [Allow](#allow)
22
+ - [Reject](#reject)
23
+ - [Custom](#custom)
24
+ - [Default](#default)
25
+ - [Validation Options](#validation-options)
26
+ - [Error Messages](#error-messages)
27
+ - [Localization](#localization)
28
+ - [OpenApi](#openapi)
29
+ - [Utils](#utils)
30
+ - [Differences with Joi](#differences-with-joi)
31
+
32
+ ## Installation
33
+
34
+ ```shell
35
+ yarn install @bedrockio/yada
36
+ ```
37
+
38
+ ## Getting Started
39
+
40
+ Validations are created by utility methods that build up a schema:\
41
+ (Note that the import `yd` is used here and below for brevity.)
42
+
43
+ ```js
44
+ import yd from '@bedrockio/yada';
45
+
46
+ const schema = yd.object({
47
+ name: yd.string().required(),
48
+ email: yd.string().required(),
49
+ age: yd.number(),
50
+ });
51
+ ```
52
+
53
+ The schema will now validate input with `validate`:
54
+
55
+ ```js
56
+ await schema.validate({
57
+ name: 'Jules',
58
+ email: 'jules@gmail.com
59
+ age: 25,
60
+ })
61
+ ```
62
+
63
+ Note that the `validate` method is asynchronous so must be awaited with `await`
64
+ or `then/catch`. A schema that fails validation will throw an error:
65
+
66
+ ```js
67
+ try {
68
+ await schema.validate({
69
+ name: 'Jules',
70
+ });
71
+ } catch (error) {
72
+ // Error is thrown as `email` is required.
73
+ console.info(error.details);
74
+ // [
75
+ // {
76
+ // field: 'email',
77
+ // message: '"email" is required.',
78
+ // },
79
+ // ],
80
+ }
81
+ ```
82
+
83
+ The thrown error object exposes a `details` property that describes validation
84
+ failures. [Type validations](#types) and `required()` will be run first and halt
85
+ execution. All other validations will be run sequentially. This means that all
86
+ validation issues will be identified up front:
87
+
88
+ ```js
89
+ const schema = yd.object({
90
+ password: yd
91
+ .string()
92
+ .custom((val) => {
93
+ if (val.length < 12) {
94
+ throw new Error('Must be 12 characters or more.');
95
+ }
96
+ })
97
+ .custom((val) => {
98
+ if (!val.match(/[0-9]/)) {
99
+ throw new Error('Must contain at least 1 number.');
100
+ }
101
+ }),
102
+ });
103
+ try {
104
+ await schema.validate({
105
+ password: 'password',
106
+ });
107
+ } catch (error) {
108
+ // error.details: [
109
+ // {
110
+ // ...
111
+ // field: 'password',
112
+ // details: [
113
+ // {
114
+ // type: 'custom',
115
+ // message: 'Must be 12 characters or more.',
116
+ // },
117
+ // {
118
+ // type: 'custom',
119
+ // message: 'Must contain at least 1 number.',
120
+ // }
121
+ // [
122
+ // },
123
+ // ],
124
+ }
125
+ ```
126
+
127
+ A validation that passes will return either the initial input or any
128
+ transformations that may apply to the schema:
129
+
130
+ ```js
131
+ const schema = yd.date();
132
+ const date = await schema.validate('2020-01-01');
133
+ console.log(date instanceof Date); // true
134
+ ```
135
+
136
+ Custom validations can transform data simply by returning a value that is not
137
+ `undefined` in the validation function:
138
+
139
+ ```js
140
+ const schema = yd.string().custom((val) => {
141
+ return val.split(',');
142
+ });
143
+ const arr = await schema.validate('a,b,c');
144
+ console.log(arr); // ['a','b','c']
145
+ console.log(Array.isArray(arr)); // true
146
+ ```
147
+
148
+ ## Types
149
+
150
+ The core validation types are:
151
+
152
+ - [String](#string)
153
+ - [Number](#number)
154
+ - [Boolean](#boolean)
155
+ - [Array](#array)
156
+ - [Object](#object)
157
+ - [Date](#date)
158
+
159
+ These perform an initial type check on validation and will halt execution of any
160
+ further validations (except `required` which is run up front). The above types
161
+ are all "basic" types as they can be serialized as JSON with the exception of
162
+ [dates](#date).
163
+
164
+ ### String
165
+
166
+ Strings are the most basic validation type. They are simple but come in many
167
+ different formats that can be validated against. Most string validations use
168
+ [validator](https://www.npmjs.com/package/validator) under the hood. Validations
169
+ that allow options will be passed through to that library.
170
+
171
+ #### Methods:
172
+
173
+ - `ascii` - Must be ASCII characters.
174
+ - `base64` - Must be base64 encoded. Allows options.
175
+ - `btc` - Must be a valid BTC address.
176
+ - `country` - Must be a valid
177
+ [ISO 3166-1 alpha 2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) code.
178
+ - `creditCard` - Must be a valid credit card number.
179
+ - `domain` - Must be a fully qualified domain name. Allows options.
180
+ - `email` - Must be a valid email address.
181
+ - `eth` - Must be a valid ETH address. Does not validate checksums.
182
+ - `hex` - Must be valid hexadecimal input.
183
+ - `ip` - Must be a valid IP address. Allows v4 or v6.
184
+ - `jwt` - Must be a valid JWT token.
185
+ - `latlng` - Must be a valid string representation of a lat/lng coordinate (ie.
186
+ `"35.686648,139.6975552"`).
187
+ - `locale` - Must be a valid locale code.
188
+ - `match` - Must match the regex pattern passed as an argument.
189
+ - `max` - Length must be less than equal to `n` characters, passed as an
190
+ argument.
191
+ - `md5` - Must be an MD5 hash.
192
+ - `min` - Length must be more than or equal to `n` characters, passed as an
193
+ argument.
194
+ - `mongo` - Must be a valid hexadecimal representation of a
195
+ [Mongo ObjectId](https://www.mongodb.com/docs/manual/reference/bson-types/#std-label-objectid).
196
+ - `password` - Must be a valid password as defined by the options object passed
197
+ as an argument:
198
+ - `minLength`
199
+ - `minLowercase`
200
+ - `minUppercase`
201
+ - `minNumbers`
202
+ - `minSymbols`
203
+ - `postalCode` - Must be a valid postal code. Accepts a `locale`.
204
+ - `sha1` - Must be a SHA1 hash.
205
+ - `slug` - Must be a URL slug, ie. a-z, numbers, or hyphen.
206
+ - `swift` - Must be a valid SWIFT (Bank Identification) code.
207
+ - `url` - Must be a valid URL. Allows options.
208
+ - `uuid` - Must be a valid UUID. Allows a `version` string.
209
+
210
+ There are also transform methods that will transform input:
211
+
212
+ - `trim` - Trims input with `String#trim`.
213
+ - `lowercase` - Transforms input into lower case. Passing `true` as the first
214
+ argument will instead error if input is not lower case.
215
+ - `uppercase` - Transforms input into upper case. Passing `true` as the first
216
+ argument will instead error if input is not upper case.
217
+
218
+ ### Number
219
+
220
+ Numbers have a handful of useful valiadtions:
221
+
222
+ #### Methods:
223
+
224
+ - `integer` - Must be an integer.
225
+ - `max` - Must be less than or equal to `n`, passed as an argument.
226
+ - `min` - Must be greater than or equal to `n`, passed as an argument.
227
+ - `multiple` - Must be a multiple of `n`, passed as an argument.
228
+ - `negative` - Must be a negative number.
229
+ - `positive` - Must be a positive number.
230
+
231
+ ### Boolean
232
+
233
+ `.boolean()` simply validates that input is a boolean value. There are no
234
+ further validation methods. Note that when `.required()` is used the input must
235
+ be a boolean, either `true` or `false`.
236
+
237
+ ### Array
238
+
239
+ Array schemas validate that input is an array. By default elements may be of any
240
+ type, however however other schemas may be passed in as arguments:
241
+
242
+ ```js
243
+ // Allows arrays containing either strings or numbers.
244
+ const schema = yd.array(yd.string(), yd.number());
245
+ ```
246
+
247
+ A single array may also be passed in place of enumerated arguments:
248
+
249
+ ```js
250
+ // Allows arrays containing either strings or numbers.
251
+ const schema = yd.array([schemas]);
252
+ ```
253
+
254
+ #### Methods:
255
+
256
+ - `max` - Length must be less than equal to `n` characters, passed as an
257
+ argument.
258
+ - `min` - Length must be more than or equal to `n` characters, passed as an
259
+ - `latlng` - Must be a 2 element array of numbers that represent latitude (`-90`
260
+ to `90`) and longitude(`-180` to `180`) coordinates.
261
+
262
+ ## Object
263
+
264
+ Object schemas validate that input is an object and further validate individual
265
+ fields:
266
+
267
+ ```js
268
+ const schema = yd.object({
269
+ name: yd.string().required(),
270
+ dob: yd.date(),
271
+ });
272
+ ```
273
+
274
+ Note that schema fields will only be validated if input exists:
275
+
276
+ ```js
277
+ // No object passed so no error thrown.
278
+ await schema.validate();
279
+
280
+ // Object passed but required field did
281
+ // not pass validation so error is thrown.
282
+ await schema.validate({
283
+ dob: '2020-01-01',
284
+ });
285
+
286
+ // Passes
287
+ await schema.validate({
288
+ name: 'Jules',
289
+ dob: '2020-01-01',
290
+ });
291
+ ```
292
+
293
+ To ensure that the object exists, simply use `.required()`.
294
+
295
+ ```js
296
+ const schema = yd
297
+ .object({
298
+ // ...
299
+ })
300
+ .required();
301
+ ```
302
+
303
+ #### Methods:
304
+
305
+ - `append` - Appends the passed argument to create a new object schema. Accepts
306
+ either an object or another `.object()` schema.
307
+
308
+ ## Date
309
+
310
+ Dates are similar to the basic types with the exception that in addition to date
311
+ objects they will also accept input that can resolve into a date. By default
312
+ this will include any string input or a number representing a timestamp in
313
+ milliseconds.
314
+
315
+ #### Methods:
316
+
317
+ - `after` - Input must resolve to a date after the supplied argument. Compare to
318
+ `min` which allows the date to be the same moment.
319
+ - `before` - Input must resolve to a date before the supplied argument. Compare
320
+ to `max` which allows the date to be the same moment.
321
+ - `future` - Input must resolve to a date in the future.
322
+ - `iso` - Input must be a string in
323
+ [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format which will resolve
324
+ into a date. Note that this format can reprsent both a date (`2020-01-01`) as
325
+ well as a datetime (`2020-01-01T00:00:00.000Z`).
326
+ - `max` - Input must resolve to a date before or equal to the supplied argument.
327
+ Compare to `before` which does not allow the input to be the same moment.
328
+ - `min` - Input must resolve to a date after or equal to the supplied argument.
329
+ Compare to `after` which does not allow the input to be the same moment.
330
+ - `past` - Input must resolve to a date in the past.
331
+ - `timestamp` - Input must be a number in milliseconds which will resolve into a
332
+ date.
333
+ - `unix - Input must be a number in seconds which will resolve into a date.
334
+
335
+ ## Common Methods
336
+
337
+ The following methods are common to all schemas.
338
+
339
+ ### Allow
340
+
341
+ Alternates are validated with the `allow` schema. This may be used to validate
342
+ literals as enums:
343
+
344
+ ```js
345
+ const schema = yd.string().allow('foo', 'bar');
346
+ ```
347
+
348
+ Or to allow alternates matching different schemas:
349
+
350
+ ```js
351
+ const schema = yd.allow(yd.string(), yd.number());
352
+ ```
353
+
354
+ An array may also be passed here as the first argument:
355
+
356
+ ```js
357
+ const schema = yd.object({
358
+ countries: yd.string.allow(countries),
359
+ });
360
+ ```
361
+
362
+ ### Reject
363
+
364
+ The `reject` schema is simply an inverse of `allow`. Anything that does not
365
+ match will be allowed through:
366
+
367
+ ```js
368
+ const schema = yd.string().reject('foo', 'bar');
369
+ await schema.validate('baz'); // pass
370
+ await schema.validate('foo'); // error!
371
+ ```
372
+
373
+ ```js
374
+ const schema = yd.reject(yd.string(), yd.number());
375
+ await schema.validate(true); // pass
376
+ await schema.validate('true'); // error!
377
+ ```
378
+
379
+ ### Custom
380
+
381
+ The `custom` schema allows for custom validations expressed in code. A custom
382
+ schema may either throw an error or transform the input by returning a value
383
+ that is not `undefined`.
384
+
385
+ ```js
386
+ const schema = yd.custom((val) => {
387
+ if (val === 'foo') {
388
+ throw new Error('"foo" is not allowed!');
389
+ } else if (val.startsWith('f')) {
390
+ return 'b' + val.slice(1);
391
+ }
392
+ });
393
+ console.info(await schema.validate('fad')); // "bad"
394
+ console.info(await schema.validate('far')); // "bar"
395
+ console.info(await schema.validate('foo')); // error thrown: "foo" is not allowed!
396
+ ```
397
+
398
+ #### Arguments
399
+
400
+ Custom validators are passed two arguments: the initial `value` and an `options`
401
+ object that contains meta information that can be helpful:
402
+
403
+ - `root`: The root value passed into the validation. This is helpful to create
404
+ complex validations that depend on other fields, etc.
405
+ - `original`: The original input passed in before transformation.
406
+ - `...rest`: Other fields specific to the schema as well as any options passed
407
+ into the schema when validating.
408
+
409
+ #### Examples
410
+
411
+ Validating input that depends on another field in an object schema:
412
+
413
+ ```js
414
+ const schema = yd.object({
415
+ age: yd.number(),
416
+ parentConsent: yd.boolean().custom((val, { root }) => {
417
+ if (!val && root.age < 13) {
418
+ throw new Error('Parent consent must be given if you are under 13.');
419
+ }
420
+ }),
421
+ });
422
+ ```
423
+
424
+ Validating input that depends on options passed in at runtime:
425
+
426
+ ```js
427
+ const schema = yd.custom((val, options) => {
428
+ if (options.foo) {
429
+ // Do something
430
+ } else {
431
+ // Do soemthing else
432
+ }
433
+ }));
434
+
435
+ await schema.validate('test', {
436
+ foo: 'bar'
437
+ })
438
+ ```
439
+
440
+ Asynchronous validation from an external service:
441
+
442
+ ```js
443
+ const schema = yd.object({
444
+ numbers: yd.string().custom(async (val, { root }) => {
445
+ try {
446
+ const info = await validateCreditCard({
447
+ number: val,
448
+ cvc: root.cvc,
449
+ });
450
+ // Return a transformed object that represents the credit card.
451
+ return info;
452
+ } catch (error) {
453
+ throw new Error('Could not validate credit card number');
454
+ }
455
+ }),
456
+ cvc: yd.string().required(),
457
+ });
458
+ ```
459
+
460
+ ### Default
461
+
462
+ The `default` method will return a default value if one is not passed:
463
+
464
+ ```js
465
+ const schema = yd.string().default('hi!');
466
+ console.log(await schema.validate()); // "hi!"
467
+ console.log(await schema.validate('hello!')); // "hello!"
468
+ ```
469
+
470
+ ## Validation Options
471
+
472
+ Validation options in Yada can be passed at runtime on validation or baked into
473
+ the schema with the `options` method. The following can be considered
474
+ equivalent:
475
+
476
+ ```js
477
+ await schema.validate(input, {
478
+ stripUnknown: true,
479
+ });
480
+ await schema
481
+ .options({
482
+ stripUnknown: true,
483
+ })
484
+ .validate(input);
485
+ ```
486
+
487
+ #### Options:
488
+
489
+ - `stripUnknown` - Strips unknown fields on object schemas which otherwise would
490
+ throw an error.
491
+ - `cast` - Casts input to its associated type. For strings and numbers this
492
+ performs a simple type coercion. For booleans `"true"` or `"1"` will be
493
+ considered `true` and `"false"` or `"0"` will be considered `false`. This
494
+ option is useful to convert query strings.
495
+
496
+ ## Error Messages
497
+
498
+ Error objects thrown on validation expose details about the validations and
499
+ fields which have errored. The `getFullMessage` method aggregates these as a
500
+ single message separated by a space:
501
+
502
+ ```js
503
+ const schema = yd.object({
504
+ email: yd.string().required(),
505
+ firstName: yd.string().required(),
506
+ lastName: yd.string().required(),
507
+ });
508
+ try {
509
+ await schema.validate({
510
+ firstName: 'Jules',
511
+ });
512
+ } catch (error) {
513
+ console.info(error.getFullMessage());
514
+ // "email" is required "lastName" is required
515
+ }
516
+ ```
517
+
518
+ The delimiter can be customized with the `delimiter` option:
519
+
520
+ ```js
521
+ console.info(
522
+ error.getFullMessage({
523
+ delimiter: '\n',
524
+ })
525
+ );
526
+ // "email" is required
527
+ // "lastName" is required
528
+ ```
529
+
530
+ Further the `natural` option will capitalize field names to correspond with form
531
+ labels:
532
+
533
+ ```js
534
+ console.info(
535
+ error.getFullMessage({
536
+ natural: true,
537
+ })
538
+ );
539
+ // "Email" is required
540
+ // "Last Name" is required
541
+ ```
542
+
543
+ ## Localization
544
+
545
+ Any error message can be localized with the `useLocalizer` method. This method
546
+ accepts either a map of error messages to their localized strings or a function
547
+ which returns that map:
548
+
549
+ ```js
550
+ yd.useLocalizer({
551
+ 'Must be at least {length} character{s}.': '{length}文字以上入力して下さい。',
552
+ 'Object failed validation.': '不正な入力がありました。',
553
+ });
554
+ ```
555
+
556
+ or
557
+
558
+ ```js
559
+ yd.useLocalizer((template) => {
560
+ // Where "getMessages" could be a function
561
+ // that returns messages for a specific locale.
562
+ return getMessages()[template];
563
+ });
564
+ ```
565
+
566
+ Any validation message matching the keys passed to the map will result in the
567
+ localized message instead. The curly braces allow for variable substitution.
568
+
569
+ A special method `getLocalizerTemplates` will aggregte used error messages to
570
+ allow quick discovery of strings that have not yet been localized:
571
+
572
+ ```js
573
+ yd.useLocalizer({
574
+ 'Must be at least {length} character{s}.': '{length}文字以上入力して下さい。',
575
+ 'Object failed validation.': '不正な入力がありました。',
576
+ });
577
+ // Error validation occuring here
578
+ yd.getLocalizedTemplates();
579
+ // {
580
+ // 'Must be at least {length} character{s}.': '{length}文字以上入力して下さい。',
581
+ // 'Object failed validation.': '不正な入力がありました。',
582
+ // 'Value must be a string.': 'Value must be a string.',
583
+ // ...etc
584
+ // }
585
+ ```
586
+
587
+ Finally an exported error object `LocalizedError` allows you to throw custom
588
+ localized error messages. Template substitution can be used here as well:
589
+
590
+ ```js
591
+ import { default as yd, LocalizedError } from '@bedrockio/yada';
592
+
593
+ const max = 5;
594
+ const schema = yd.custom((val) => {
595
+ if (val > chars) {
596
+ throw new LocalizedError('Cannot be greater than {max}.', {
597
+ max,
598
+ });
599
+ }
600
+ });
601
+ ```
602
+
603
+ ## OpenApi
604
+
605
+ One of the benefits of Yada is built-in OpenApi integration. Any schema can
606
+ describe itself in OpenApi format including complex nested schemas.
607
+
608
+ ```js
609
+ yd.string().allow('foo', 'bar').toOpenApi();
610
+ // {
611
+ // type: 'string',
612
+ // enum: ['foo', 'bar'],
613
+ // }
614
+ ```
615
+
616
+ The `tag` method allows tagging custom OpenApi fields:
617
+
618
+ ```js
619
+ yd.string().tag({
620
+ description: 'my description!',
621
+ x-field: 'my custom field!',
622
+ }).toOpenApi();
623
+ // {
624
+ // type: 'string',
625
+ // description: 'my description!',
626
+ // x-field: 'my custom field!!',
627
+ // }
628
+ ```
629
+
630
+ The `description` method is a shortcut:
631
+
632
+ ```js
633
+ yd.string().description('my description!').toOpenApi();
634
+ // {
635
+ // type: 'string',
636
+ // description: 'my description!',
637
+ // }
638
+ ```
639
+
640
+ ## Utils
641
+
642
+ Basic utility methods:
643
+
644
+ - `isSchema`: returns `true` if the object passed is a schema.
645
+ - `isSchemaError`: returns `true` if the error object is a schema error.
646
+ - `useLocalizer`: Allows [localization](#localization) of error messages.
647
+ - `getLocalizerTemplates`: Allows discovery of messages for
648
+ [localization](#localization).
649
+ - `LocalizedError`: An error object that can be thrown in custom validations to
650
+ allow [localization](#localization).
651
+
652
+ ## Differences with Joi
653
+
654
+ Yada is intentionally kept extremely simple. Complex validations are expressed
655
+ in code rather than requiring complex API knowledge. For example (from the Joi
656
+ docs):
657
+
658
+ ```js
659
+ // Joi schema
660
+ const schema = Joi.object({
661
+ a: Joi.any()
662
+ .valid('x')
663
+ .when('b', {
664
+ is: Joi.exist(),
665
+ then: Joi.valid('y'),
666
+ otherwise: Joi.valid('z'),
667
+ })
668
+ .when('c', { is: Joi.number().min(10), then: Joi.forbidden() }),
669
+ b: Joi.any(),
670
+ c: Joi.number(),
671
+ });
672
+ ```
673
+
674
+ ```js
675
+ // equivalent yada schema
676
+ const schema = yd.object({
677
+ a: yd
678
+ .custom((val, { root }) => {
679
+ if (root.b && val !== 'y') {
680
+ throw new Error('x must be y when b is passed');
681
+ } else if (val !== 'z') {
682
+ throw new Error('x must be z when b is not passed');
683
+ }
684
+ })
685
+ .custom((val, { root }) => {
686
+ if (typeof root.c > 10 && val) {
687
+ throw new Error('x may not be passed when c is more than 10');
688
+ }
689
+ }),
690
+ b: yd.string(),
691
+ c: yd.number(),
692
+ });
693
+ ```
694
+
695
+ Note: yada has no "any" equivalent. schemas must either define their types or
696
+ use "allow" for alternate types.
697
+
698
+ For a few more lines of code the same schema can be defined simply as readable
699
+ Javascript, complete with custom error messages.
700
+
701
+ Additionally, schemas may be validated inside other custom schemas, as they are
702
+ simply functions that throw an error or (optionally) return a transformed value.
703
+ This makes even custom schema composition simple:
704
+
705
+ ```js
706
+ // Joi schema
707
+ const states = Joi.valid(UsStates);
708
+ const provinces = Joi.valid(JapanProvinces);
709
+ const schema = Joi.object({
710
+ region: Joi.string().when('country', {
711
+ is: 'japan',
712
+ then: provinces,
713
+ otherwise: states,
714
+ }),
715
+ });
716
+ ```
717
+
718
+ ```js
719
+ // equivalent yada schema
720
+ const states = yd.allow(UsStates);
721
+ const provinces = yd.allow(JapanProvinces);
722
+ const schema = yd.object({
723
+ region: yd.string().custom(async (val, { root }) => {
724
+ const schema = root.country === 'japan' ? provinces : states;
725
+ await schema.validate(val);
726
+ }),
727
+ });
728
+ ```
729
+
730
+ Since all custom validators are simply functions that throw errors, the above
731
+ can also use a simple `try/catch` to throw custom error messages:
732
+
733
+ ```js
734
+ // equivalent yada schema
735
+ const states = yd.allow(UsStates);
736
+ const provinces = yd.allow(JapanProvinces);
737
+ const schema = yd.object({
738
+ region: yd.string().custom(async (val, { root }) => {
739
+ const schema = root.country === 'japan' ? provinces : states;
740
+ try {
741
+ await schema.validate(val);
742
+ } catch {
743
+ throw new Error('"region" is invalid.');
744
+ }
745
+ }),
746
+ });
747
+ ```
748
+
749
+ This can be useful either to add context or to strip it away, for example to
750
+ hide allowed values if they are private.