1# `@kbn/config-schema` — The Kibana config validation library 2 3`@kbn/config-schema` is a TypeScript library inspired by Joi and designed to allow run-time validation of the 4Kibana configuration entries providing developers with a fully typed model of the validated data. 5 6## Table of Contents 7 8- [Why `@kbn/config-schema`?](#why-kbnconfig-schema) 9- [Schema building blocks](#schema-building-blocks) 10 - [Basic types](#basic-types) 11 - [`schema.string()`](#schemastring) 12 - [`schema.number()`](#schemanumber) 13 - [`schema.boolean()`](#schemaboolean) 14 - [`schema.literal()`](#schemaliteral) 15 - [`schema.buffer()`](#schemabuffer) 16 - [`schema.stream()`](#schemastream) 17 - [Composite types](#composite-types) 18 - [`schema.arrayOf()`](#schemaarrayof) 19 - [`schema.object()`](#schemaobject) 20 - [`schema.recordOf()`](#schemarecordof) 21 - [`schema.mapOf()`](#schemamapof) 22 - [Advanced types](#advanced-types) 23 - [`schema.oneOf()`](#schemaoneof) 24 - [`schema.any()`](#schemaany) 25 - [`schema.maybe()`](#schemamaybe) 26 - [`schema.nullable()`](#schemanullable) 27 - [`schema.never()`](#schemanever) 28 - [`schema.uri()`](#schemauri) 29 - [`schema.byteSize()`](#schemabytesize) 30 - [`schema.duration()`](#schemaduration) 31 - [`schema.conditional()`](#schemaconditional) 32 - [References](#references) 33 - [`schema.contextRef()`](#schemacontextref) 34 - [`schema.siblingRef()`](#schemasiblingref) 35- [Custom validation](#custom-validation) 36- [Default values](#default-values) 37 38## Why `@kbn/config-schema`? 39 40Validation of externally supplied data is very important for Kibana. Especially if this data is used to configure how it operates. 41 42There are a number of reasons why we decided to roll our own solution for the configuration validation: 43 44* **Limited API surface** - having a future rich library is awesome, but it's a really hard task to audit such library and make sure everything is sane and secure enough. As everyone knows complexity is the enemy of security and hence we'd like to have a full control over what exactly we expose and commit to maintain. 45* **Custom error messages** - detailed validation error messages are a great help to developers, but at the same time they can contain information that's way too sensitive to expose to everyone. We'd like to control these messages and make them only as detailed as really needed. For example, we don't want validation error messages to contain the passwords for internal users to show-up in the logs. These logs are commonly ingested into Elasticsearch, and accessible to a large number of users which shouldn't have access to the internal user's password. 46* **Type information** - having run-time guarantees is great, but additionally having compile-time guarantees is even better. We'd like to provide developers with a fully typed model of the validated data so that it's harder to misuse it _after_ validation. 47* **Upgradability** - no matter how well a validation library is implemented, it will have bugs and may need to be improved at some point anyway. Some external libraries are very well supported, some aren't or won't be in the future. It's always a risk to depend on an external party with their own release cadence when you need to quickly fix a security vulnerability in a patch version. We'd like to have a better control over lifecycle of such an important piece of our codebase. 48 49## Schema building blocks 50 51The schema is composed of one or more primitives depending on the shape of the data you'd like to validate. 52 53```typescript 54const simpleStringSchema = schema.string(); 55const moreComplexObjectSchema = schema.object({ name: schema.string() }); 56``` 57 58Every schema instance has a `validate` method that is used to perform a validation of the data according to the schema. This method accepts three arguments: 59 60* `data: any` - **required**, data to be validated with the schema 61* `context: Record<string, any>` - **optional**, object whose properties can be referenced by the [context references](#schemacontextref) 62* `namespace: string` - **optional**, arbitrary string that is used to prefix every error message thrown during validation 63 64```typescript 65const valueSchema = schema.object({ 66 isEnabled: schema.boolean(), 67 env: schema.string({ defaultValue: schema.contextRef('envName') }), 68}); 69 70expect(valueSchema.validate({ isEnabled: true, env: 'prod' })).toEqual({ 71 isEnabled: true, 72 env: 'prod', 73}); 74 75// Use default value for `env` from context via reference 76expect(valueSchema.validate({ isEnabled: true }, { envName: 'staging' })).toEqual({ 77 isEnabled: true, 78 env: 'staging', 79}); 80 81// Fail because of type mismatch 82expect(() => 83 valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }) 84).toThrowError( 85 '[isEnabled]: expected value of type [boolean] but got [string]' 86); 87 88// Fail because of type mismatch and prefix error with a custom namespace 89expect(() => 90 valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }, 'configuration') 91).toThrowError( 92 '[configuration.isEnabled]: expected value of type [boolean] but got [string]' 93); 94``` 95 96__Notes:__ 97* `validate` method throws as soon as the first schema violation is encountered, no further validation is performed. 98* when you retrieve configuration within a Kibana plugin `validate` function is called by the Core automatically providing appropriate namespace and context variables (environment name, package info etc.). 99 100### Basic types 101 102#### `schema.string()` 103 104Validates input data as a string. 105 106__Output type:__ `string` 107 108__Options:__ 109 * `defaultValue: string | Reference<string> | (() => string)` - defines a default value, see [Default values](#default-values) section for more details. 110 * `validate: (value: string) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 111 * `minLength: number` - defines a minimum length the string should have. 112 * `maxLength: number` - defines a maximum length the string should have. 113 * `hostname: boolean` - indicates whether the string should be validated as a valid hostname (per [RFC 1123](https://tools.ietf.org/html/rfc1123)). 114 115__Usage:__ 116```typescript 117const valueSchema = schema.string({ maxLength: 10 }); 118``` 119 120__Notes:__ 121* By default `schema.string()` allows empty strings, to prevent that use non-zero value for `minLength` option. 122* To validate a string using a regular expression use a custom validator function, see [Custom validation](#custom-validation) section for more details. 123 124#### `schema.number()` 125 126Validates input data as a number. 127 128__Output type:__ `number` 129 130__Options:__ 131 * `defaultValue: number | Reference<number> | (() => number)` - defines a default value, see [Default values](#default-values) section for more details. 132 * `validate: (value: number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 133 * `min: number` - defines a minimum value the number should have. 134 * `max: number` - defines a maximum value the number should have. 135 136__Usage:__ 137```typescript 138const valueSchema = schema.number({ max: 10 }); 139``` 140 141__Notes:__ 142* The `schema.number()` also supports a string as input if it can be safely coerced into number. 143 144#### `schema.boolean()` 145 146Validates input data as a boolean. 147 148__Output type:__ `boolean` 149 150__Options:__ 151 * `defaultValue: boolean | Reference<boolean> | (() => boolean)` - defines a default value, see [Default values](#default-values) section for more details. 152 * `validate: (value: boolean) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 153 154__Usage:__ 155```typescript 156const valueSchema = schema.boolean({ defaultValue: false }); 157``` 158 159__Notes:__ 160* The `schema.boolean()` also supports a string as input if it equals `'true'` or `'false'` (case-insensitive). 161 162#### `schema.literal()` 163 164Validates input data as a [string](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types), [numeric](https://www.typescriptlang.org/docs/handbook/advanced-types.html#numeric-literal-types) or boolean literal. 165 166__Output type:__ `string`, `number` or `boolean` literals 167 168__Options:__ 169 * `defaultValue: TLiteral | Reference<TLiteral> | (() => TLiteral)` - defines a default value, see [Default values](#default-values) section for more details. 170 * `validate: (value: TLiteral) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 171 172__Usage:__ 173```typescript 174const valueSchema = [ 175 schema.literal('stringLiteral'), 176 schema.literal(100500), 177 schema.literal(false), 178]; 179``` 180 181#### `schema.buffer()` 182 183Validates input data as a NodeJS `Buffer`. 184 185__Output type:__ `Buffer` 186 187__Options:__ 188 * `defaultValue: TBuffer | Reference<TBuffer> | (() => TBuffer)` - defines a default value, see [Default values](#default-values) section for more details. 189 * `validate: (value: TBuffer) => Buffer | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 190 191__Usage:__ 192```typescript 193const valueSchema = schema.buffer({ defaultValue: Buffer.from('Hi, there!') }); 194``` 195 196#### `schema.stream()` 197 198Validates input data as a NodeJS `stream`. 199 200__Output type:__ `Stream`, `Readable` or `Writtable` 201 202__Options:__ 203 * `defaultValue: TStream | Reference<TStream> | (() => TStream)` - defines a default value, see [Default values](#default-values) section for more details. 204 * `validate: (value: TStream) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 205 206__Usage:__ 207```typescript 208const valueSchema = schema.stream({ defaultValue: new Stream() }); 209``` 210 211### Composite types 212 213#### `schema.arrayOf()` 214 215Validates input data as a homogeneous array with the values being validated against predefined schema. 216 217__Output type:__ `TValue[]` 218 219__Options:__ 220 * `defaultValue: TValue[] | Reference<TValue[]> | (() => TValue[])` - defines a default value, see [Default values](#default-values) section for more details. 221 * `validate: (value: TValue[]) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 222 * `minSize: number` - defines a minimum size the array should have. 223 * `maxSize: number` - defines a maximum size the array should have. 224 225__Usage:__ 226```typescript 227const valueSchema = schema.arrayOf(schema.number()); 228``` 229 230__Notes:__ 231* The `schema.arrayOf()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is an array. 232 233#### `schema.object()` 234 235Validates input data as an object with a predefined set of properties. 236 237__Output type:__ `{ [K in keyof TProps]: TypeOf<TProps[K]> } as TObject` 238 239__Options:__ 240 * `defaultValue: TObject | Reference<TObject> | (() => TObject)` - defines a default value, see [Default values](#default-values) section for more details. 241 * `validate: (value: TObject) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 242 * `unknowns: 'allow' | 'ignore' | 'forbid'` - indicates whether unknown object properties should be allowed, ignored, or forbidden. It's `forbid` by default. 243 244__Usage:__ 245```typescript 246const valueSchema = schema.object({ 247 isEnabled: schema.boolean({ defaultValue: false }), 248 name: schema.string({ minLength: 10 }), 249}); 250``` 251 252__Notes:__ 253* Using `unknowns: 'allow'` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. 254* Currently `schema.object()` always has a default value of `{}`, but this may change in the near future. Try to not rely on this behaviour and specify default value explicitly or use `schema.maybe()` if the value is optional. 255* `schema.object()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is a plain object. 256 257#### `schema.recordOf()` 258 259Validates input data as an object with the keys and values being validated against predefined schema. 260 261__Output type:__ `Record<TKey, TValue>` 262 263__Options:__ 264 * `defaultValue: Record<TKey, TValue> | Reference<Record<TKey, TValue>> | (() => Record<TKey, TValue>)` - defines a default value, see [Default values](#default-values) section for more details. 265 * `validate: (value: Record<TKey, TValue>) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 266 267__Usage:__ 268```typescript 269const valueSchema = schema.recordOf(schema.string(), schema.number()); 270``` 271 272__Notes:__ 273* You can use a union of literal types as a record's key schema to restrict record to a specific set of keys, e.g. `schema.oneOf([schema.literal('isEnabled'), schema.literal('name')])`. 274* `schema.recordOf()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is a plain object. 275 276#### `schema.mapOf()` 277 278Validates input data as a map with the keys and values being validated against the predefined schema. 279 280__Output type:__ `Map<TKey, TValue>` 281 282__Options:__ 283 * `defaultValue: Map<TKey, TValue> | Reference<Map<TKey, TValue>> | (() => Map<TKey, TValue>)` - defines a default value, see [Default values](#default-values) section for more details. 284 * `validate: (value: Map<TKey, TValue>) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 285 286__Usage:__ 287```typescript 288const valueSchema = schema.mapOf(schema.string(), schema.number()); 289``` 290 291__Notes:__ 292* You can use a union of literal types as a record's key schema to restrict record to a specific set of keys, e.g. `schema.oneOf([schema.literal('isEnabled'), schema.literal('name')])`. 293* `schema.mapOf()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is a plain object. 294 295### Advanced types 296 297#### `schema.oneOf()` 298 299Allows a list of alternative schemas to validate input data against. 300 301__Output type:__ `TValue1 | TValue2 | TValue3 | ..... as TUnion` 302 303__Options:__ 304 * `defaultValue: TUnion | Reference<TUnion> | (() => TUnion)` - defines a default value, see [Default values](#default-values) section for more details. 305 * `validate: (value: TUnion) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 306 307__Usage:__ 308```typescript 309const valueSchema = schema.oneOf([schema.literal('∞'), schema.number()]); 310``` 311 312__Notes:__ 313* Since the result data type is a type union you should use various TypeScript type guards to get the exact type. 314 315#### `schema.any()` 316 317Indicates that input data shouldn't be validated and returned as is. 318 319__Output type:__ `any` 320 321__Options:__ 322 * `defaultValue: any | Reference<any> | (() => any)` - defines a default value, see [Default values](#default-values) section for more details. 323 * `validate: (value: any) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 324 325__Usage:__ 326```typescript 327const valueSchema = schema.any(); 328``` 329 330__Notes:__ 331* `schema.any()` is essentially an escape hatch for the case when your data can __really__ have any type and should be avoided at all costs. 332 333#### `schema.maybe()` 334 335Indicates that input data is optional and may not be present. 336 337__Output type:__ `T | undefined` 338 339__Usage:__ 340```typescript 341const valueSchema = schema.maybe(schema.string()); 342``` 343 344__Notes:__ 345* Don't use `schema.maybe()` if a nested type defines a default value. 346 347#### `schema.nullable()` 348 349Indicates that input data is optional and defaults to `null` if it's not present. 350 351__Output type:__ `T | null` 352 353__Usage:__ 354```typescript 355const valueSchema = schema.nullable(schema.string()); 356``` 357 358__Notes:__ 359* `schema.nullable()` also treats explicitly specified `null` as a valid input. 360 361#### `schema.never()` 362 363Indicates that input data is forbidden. 364 365__Output type:__ `never` 366 367__Usage:__ 368```typescript 369const valueSchema = schema.never(); 370``` 371 372__Notes:__ 373* `schema.never()` has a very limited application and usually used within [conditional schemas](#schemaconditional) to fully or partially forbid input data. 374 375#### `schema.uri()` 376 377Validates input data as a proper URI string (per [RFC 3986](https://tools.ietf.org/html/rfc3986)). 378 379__Output type:__ `string` 380 381__Options:__ 382 * `defaultValue: string | Reference<string> | (() => string)` - defines a default value, see [Default values](#default-values) section for more details. 383 * `validate: (value: string) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 384 * `scheme: string | string[]` - limits allowed URI schemes to the one(s) defined here. 385 386__Usage:__ 387```typescript 388const valueSchema = schema.uri({ scheme: 'https' }); 389``` 390 391__Notes:__ 392* Prefer using `schema.uri()` for all URI validations even though it may be possible to replicate it with a custom validator for `schema.string()`. 393 394#### `schema.byteSize()` 395 396Validates input data as a proper digital data size. 397 398__Output type:__ `ByteSizeValue` 399 400__Options:__ 401 * `defaultValue: ByteSizeValue | string | number | Reference<ByteSizeValue | string | number> | (() => ByteSizeValue | string | number)` - defines a default value, see [Default values](#default-values) section for more details. 402 * `validate: (value: ByteSizeValue | string | number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 403 * `min: ByteSizeValue | string | number` - defines a minimum value the size should have. 404 * `max: ByteSizeValue | string | number` - defines a maximum value the size should have. 405 406__Usage:__ 407```typescript 408const valueSchema = schema.byteSize({ min: '3kb' }); 409``` 410 411__Notes:__ 412* The string value for `schema.byteSize()` and its options supports the following optional suffixes: `b`, `kb`, `mb`, `gb` and `tb`. The default suffix is `b`. 413* The number value is treated as a number of bytes and hence should be a positive integer, e.g. `100` is equal to `'100b'`. 414* Currently you cannot specify zero bytes with a string format and should use number `0` instead. 415 416#### `schema.duration()` 417 418Validates input data as a proper [duration](https://momentjs.com/docs/#/durations/). 419 420__Output type:__ `moment.Duration` 421 422__Options:__ 423 * `defaultValue: moment.Duration | string | number | Reference<moment.Duration | string | number> | (() => moment.Duration | string | number)` - defines a default value, see [Default values](#default-values) section for more details. 424 * `validate: (value: moment.Duration | string | number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 425 426__Usage:__ 427```typescript 428const valueSchema = schema.duration({ defaultValue: '70ms' }); 429``` 430 431__Notes:__ 432* The string value for `schema.duration()` supports the following optional suffixes: `ms`, `s`, `m`, `h`, `d`, `w`, `M` and `Y`. The default suffix is `ms`. 433* The number value is treated as a number of milliseconds and hence should be a positive integer, e.g. `100` is equal to `'100ms'`. 434 435#### `schema.conditional()` 436 437Allows a specified condition that is evaluated _at the validation time_ and results in either one or another input validation schema. 438 439The first argument is always a [reference](#references) while the second one can be: 440* another reference, in this cases both references are "dereferenced" and compared 441* schema, in this case the schema is used to validate "dereferenced" value of the first reference 442* value, in this case "dereferenced" value of the first reference is compared to that value 443 444The third argument is a schema that should be used if the result of the aforementioned comparison evaluates to `true`, otherwise `schema.conditional()` should fallback 445to the schema provided as the fourth argument. 446 447__Output type:__ `TTrueResult | TFalseResult` 448 449__Options:__ 450 * `defaultValue: TTrueResult | TFalseResult | Reference<TTrueResult | TFalseResult> | (() => TTrueResult | TFalseResult` - defines a default value, see [Default values](#default-values) section for more details. 451 * `validate: (value: TTrueResult | TFalseResult) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. 452 453__Usage:__ 454```typescript 455const valueSchema = schema.object({ 456 key: schema.oneOf([schema.literal('number'), schema.literal('string')]), 457 value: schema.conditional(schema.siblingRef('key'), 'number', schema.number(), schema.string()), 458}); 459``` 460 461__Notes:__ 462* Conditional schemas may be hard to read and understand and hence should be used only sparingly. 463 464### References 465 466#### `schema.contextRef()` 467 468Defines a reference to the value specified through the validation context. Context reference is only used as part of a [conditional schema](#schemaconditional) or as a default value for any other schema. 469 470__Output type:__ `TReferenceValue` 471 472__Usage:__ 473```typescript 474const valueSchema = schema.object({ 475 env: schema.string({ defaultValue: schema.contextRef('envName') }), 476}); 477valueSchema.validate({}, { envName: 'dev' }); 478``` 479 480__Notes:__ 481* The `@kbn/config-schema` neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type. 482* The root context that Kibana provides during config validation includes lots of useful properties like `environment name` that can be used to provide a strict schema for production and more relaxed one for development. 483 484#### `schema.siblingRef()` 485 486Defines a reference to the value of the sibling key. Sibling references are only used a part of [conditional schema](#schemaconditional) or as a default value for any other schema. 487 488__Output type:__ `TReferenceValue` 489 490__Usage:__ 491```typescript 492const valueSchema = schema.object({ 493 node: schema.object({ tag: schema.string() }), 494 env: schema.string({ defaultValue: schema.siblingRef('node.tag') }), 495}); 496``` 497 498__Notes:__ 499* The `@kbn/config-schema` neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type. 500 501## Custom validation 502 503Using built-in schema primitives may not be enough in some scenarios or sometimes the attempt to model complex schemas with built-in primitives only may result in unreadable code. 504For these cases `@kbn/config-schema` provides a way to specify a custom validation function for almost any schema building block through the `validate` option. 505 506For example `@kbn/config-schema` doesn't have a dedicated primitive for the `RegExp` based validation currently, but you can easily do that with a custom `validate` function: 507 508```typescript 509const valueSchema = schema.string({ 510 minLength: 3, 511 validate(value) { 512 if (!/^[a-z0-9_-]+$/.test(value)) { 513 return `must be lower case, a-z, 0-9, '_', and '-' are allowed`; 514 } 515 }, 516}); 517 518// ...or if you use that construct a lot... 519 520const regexSchema = (regex: RegExp) => schema.string({ 521 validate: value => regex.test(value) ? undefined : `must match "${regex.toString()}"`, 522}); 523const valueSchema = regexSchema(/^[a-z0-9_-]+$/); 524``` 525 526Custom validation function is run _only after_ all built-in validations passed. It should either return a `string` as an error message 527to denote the failed validation or not return anything at all (`void`) otherwise. Please also note that `validate` function is synchronous. 528 529Another use case for custom validation functions is when the schema depends on some run-time data: 530 531```typescript 532const gesSchema = randomRunTimeSeed => schema.string({ 533 validate: value => value !== randomRunTimeSeed ? 'value is not allowed' : undefined 534}); 535 536const schema = gesSchema('some-random-run-time-data'); 537``` 538 539## Default values 540 541If you have an optional config field that you can have a default value for you may want to consider using dedicated `defaultValue` option to not 542deal with "defined or undefined"-like checks all over the place in your code. You have three options to provide a default value for almost any schema primitive: 543 544* plain value that's known at the compile time 545* [reference](#references) to a value that will be "dereferenced" at the validation time 546* function that is invoked at the validation time and returns a plain value 547 548```typescript 549const valueSchemaWithPlainValueDefault = schema.string({ defaultValue: 'n/a' }); 550const valueSchemaWithReferencedValueDefault = schema.string({ defaultValue: schema.contextRef('env') }); 551const valueSchemaWithFunctionEvaluatedDefault = schema.string({ defaultValue: () => Math.random().toString() }); 552``` 553 554__Notes:__ 555* `@kbn/config-schema` neither validates nor coerces default value and developer is responsible for making sure that it has the appropriate type. 556