src/platform/packages/shared/kbn-config-schema/README.md
@kbn/config-schema — The Kibana config validation library@kbn/config-schema is a TypeScript library inspired by Joi and designed to allow run-time validation of the
Kibana configuration entries providing developers with a fully typed model of the validated data.
@kbn/config-schema — The Kibana config validation library
@kbn/config-schema?Validation of externally supplied data is very important for Kibana. Especially if this data is used to configure how it operates.
There are a number of reasons why we decided to roll our own solution for the configuration validation:
The schema is composed of one or more primitives depending on the shape of the data you'd like to validate.
const simpleStringSchema = schema.string();
const moreComplexObjectSchema = schema.object({ name: schema.string() });
Every 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:
data: any - required, data to be validated with the schemacontext: Record<string, any> - optional, object whose properties can be referenced by the context referencesnamespace: string - optional, arbitrary string that is used to prefix every error message thrown during validationvalidationOptions: SchemaValidationOptions - optional, global options to modify the default validation behavior
stripUnknownKeys: boolean - optional, when true, it changes the default unknowns: 'forbid' to behave like unknowns: 'ignore'. This change of behavior only occurs in schemas without an explicit unknowns option. Refer to schema.object() for more information about the unknowns option.const valueSchema = schema.object({
isEnabled: schema.boolean(),
env: schema.string({ defaultValue: schema.contextRef('envName') }),
});
expect(valueSchema.validate({ isEnabled: true, env: 'prod' })).toEqual({
isEnabled: true,
env: 'prod',
});
// Use default value for `env` from context via reference
expect(valueSchema.validate({ isEnabled: true }, { envName: 'staging' })).toEqual({
isEnabled: true,
env: 'staging',
});
// Fail because of type mismatch
expect(() =>
valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' })
).toThrowError(
'[isEnabled]: expected value of type [boolean] but got [string]'
);
// Fail because of type mismatch and prefix error with a custom namespace
expect(() =>
valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }, 'configuration')
).toThrowError(
'[configuration.isEnabled]: expected value of type [boolean] but got [string]'
);
Notes:
validate method throws as soon as the first schema violation is encountered, no further validation is performed.validate function is called by the Core automatically providing appropriate namespace and context variables (environment name, package info etc.).schema.string()Validates input data as a string.
Output type: string
Options:
defaultValue: string | Reference<string> | (() => string) - defines a default value, see Default values section for more details.validate: (value: string) => string | void - defines a custom validator function, see Custom validation section for more details.minLength: number - defines a minimum length the string should have.maxLength: number - defines a maximum length the string should have.hostname: boolean - indicates whether the string should be validated as a valid hostname (per RFC 1123).Usage:
const valueSchema = schema.string({ maxLength: 10 });
Notes:
schema.string() allows empty strings, to prevent that use non-zero value for minLength option.schema.number()Validates input data as a number.
Output type: number
Options:
defaultValue: number | Reference<number> | (() => number) - defines a default value, see Default values section for more details.validate: (value: number) => string | void - defines a custom validator function, see Custom validation section for more details.min: number - defines a minimum value the number should have.max: number - defines a maximum value the number should have.unsafe: boolean - if true, will accept unsafe numbers (integers > 2^53).Usage:
const valueSchema = schema.number({ max: 10 });
Notes:
schema.number() also supports a string as input if it can be safely coerced into number.schema.boolean()Validates input data as a boolean.
Output type: boolean
Options:
defaultValue: boolean | Reference<boolean> | (() => boolean) - defines a default value, see Default values section for more details.validate: (value: boolean) => string | void - defines a custom validator function, see Custom validation section for more details.Usage:
const valueSchema = schema.boolean({ defaultValue: false });
Notes:
schema.boolean() also supports a string as input if it equals 'true' or 'false' (case-insensitive).schema.literal()Validates input data as a string, numeric or boolean literal.
Output type: string, number or boolean literals
Options:
defaultValue: TLiteral | Reference<TLiteral> | (() => TLiteral) - defines a default value, see Default values section for more details.validate: (value: TLiteral) => string | void - defines a custom validator function, see Custom validation section for more details.Usage:
const valueSchema = [
schema.literal('stringLiteral'),
schema.literal(100500),
schema.literal(false),
];
schema.buffer()Validates input data as a NodeJS Buffer.
Output type: Buffer
Options:
defaultValue: TBuffer | Reference<TBuffer> | (() => TBuffer) - defines a default value, see Default values section for more details.validate: (value: TBuffer) => Buffer | void - defines a custom validator function, see Custom validation section for more details.Usage:
const valueSchema = schema.buffer({ defaultValue: Buffer.from('Hi, there!') });
schema.stream()Validates input data as a NodeJS stream.
Output type: Stream, Readable or Writtable
Options:
defaultValue: TStream | Reference<TStream> | (() => TStream) - defines a default value, see Default values section for more details.validate: (value: TStream) => string | void - defines a custom validator function, see Custom validation section for more details.Usage:
const valueSchema = schema.stream({ defaultValue: new Stream() });
schema.arrayOf()Validates input data as a homogeneous array with the values being validated against predefined schema.
Output type: TValue[]
Options:
defaultValue: TValue[] | Reference<TValue[]> | (() => TValue[]) - defines a default value, see Default values section for more details.validate: (value: TValue[]) => string | void - defines a custom validator function, see Custom validation section for more details.minSize: number - defines a minimum size the array should have.maxSize: number - defines a maximum size the array should have.unknowns: 'ignore' | 'forbid' - indicates whether unknown properties in nested objects should be ignored or forbidden. It is forbid by default unless the global validation option stripUnknownKeys is set to true when calling validate().Usage:
const valueSchema = schema.arrayOf(schema.number());
Notes:
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.schema.object()Validates input data as an object with a predefined set of properties.
Output type: { [K in keyof TProps]: TypeOf<TProps[K]> } as TObject
Options:
defaultValue: TObject | Reference<TObject> | (() => TObject) - defines a default value, see Default values section for more details.validate: (value: TObject) => string | void - defines a custom validator function, see Custom validation section for more details.unknowns: 'allow' | 'ignore' | 'forbid' - indicates whether unknown object properties and sub-properties should be allowed, ignored, or forbidden. It is forbid by default unless the global validation option stripUnknownKeys is set to true when calling validate(). Refer to the validate() API options to learn about stripUnknownKeys.Usage:
const valueSchema = schema.object({
isEnabled: schema.boolean({ defaultValue: false }),
name: schema.string({ minLength: 10 }),
});
Specific methods:
extends - allows to extend an existing object schema, see extending object schemasNotes:
unknowns: 'allow' is discouraged and should only be used in exceptional circumstances. Consider using schema.recordOf() instead.unknowns: 'allow' | 'ignore' | 'forbid' applies to the entire tree of sub-objects. If you want this option to apply only to the properties in first level, make sure to override this option by setting a new unknowns option in the child schema.object()s.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.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.schema.recordOf()Validates input data as an object with the keys and values being validated against predefined schema.
Output type: Record<TKey, TValue>
Options:
defaultValue: Record<TKey, TValue> | Reference<Record<TKey, TValue>> | (() => Record<TKey, TValue>) - defines a default value, see Default values section for more details.validate: (value: Record<TKey, TValue>) => string | void - defines a custom validator function, see Custom validation section for more details.unknowns: 'ignore' | 'forbid' - indicates whether unknown properties in nested objects should be ignored or forbidden. It is forbid by default unless the global validation option stripUnknownKeys is set to true when calling validate().Usage:
const valueSchema = schema.recordOf(schema.string(), schema.number());
Notes:
schema.oneOf([schema.literal('isEnabled'), schema.literal('name')]).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.schema.mapOf()Validates input data as a map with the keys and values being validated against the predefined schema.
Output type: Map<TKey, TValue>
Options:
defaultValue: Map<TKey, TValue> | Reference<Map<TKey, TValue>> | (() => Map<TKey, TValue>) - defines a default value, see Default values section for more details.validate: (value: Map<TKey, TValue>) => string | void - defines a custom validator function, see Custom validation section for more details.unknowns: 'ignore' | 'forbid' - indicates whether unknown properties in nested objects should be ignored or forbidden. It is forbid by default unless the global validation option stripUnknownKeys is set to true when calling validate().Usage:
const valueSchema = schema.mapOf(schema.string(), schema.number());
Notes:
schema.oneOf([schema.literal('isEnabled'), schema.literal('name')]).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.schema.intersection() / schema.allOf()Creates an object schema being the intersection of the provided object schemas.
Note that schema construction will throw an error if some of the intersection schema share the same key(s).
See the documentation for schema.object.
Options:
defaultValue: TObject | Reference<TObject> | (() => TObject) - defines a default value, see Default values section for more details.validate: (value: TObject) => string | void - defines a custom validator function, see Custom validation section for more details.unknowns: 'allow' | 'ignore' | 'forbid' - indicates whether unknown object properties should be allowed, ignored, or forbidden. It's forbid by default.Usage:
const mySchema = schema.intersection([
schema.object({
someKey: schema.string(),
}),
schema.object({
anotherKey: schema.string(),
})
]);
schema.oneOf()Allows a list of alternative schemas to validate input data against.
Output type: TValue1 | TValue2 | TValue3 | ..... as TUnion
Options:
defaultValue: TUnion | Reference<TUnion> | (() => TUnion) - defines a default value, see Default values section for more details.validate: (value: TUnion) => string | void - defines a custom validator function, see Custom validation section for more details.Usage:
const valueSchema = schema.oneOf([schema.literal('∞'), schema.number()]);
Notes:
unknowns option since this is implemented on top of joi.alternatives(), and it doesn't accept this option.schema.discriminatedUnion()Allows a list of alternative object schemas to validate input data against, using a common discriminator property to determine which schema to use.
Output type: TObject1 | TObject2 | TObject3 | ..... as TUnion
Options:
defaultValue: TUnion | Reference<TUnion> | (() => TUnion) - defines a default value, see Default values section for more details.validate: (value: TUnion) => string | void - defines a custom validator function, see Custom validation section for more details.Usage:
const valueSchema = schema.discriminatedUnion('type', [
schema.object({ type: schema.literal('str'), value: schema.string() }),
schema.object({ type: schema.literal('num'), value: schema.number() }),
schema.object({ type: schema.literal('bool'), value: schema.boolean() }),
]);
// Valid inputs:
// { type: 'str', value: 'hello' }
// { type: 'num', value: 123 }
// { type: 'bool', value: true }
Notes:
schema.literal() with a unique string value.schema.string()) for the discriminator property. Only one fallback schema is allowed.schema.oneOf(), this provides better error messages since it can identify which schema variant was intended based on the discriminator value.schema.any()Indicates that input data shouldn't be validated and returned as is.
Output type: any
Options:
defaultValue: any | Reference<any> | (() => any) - defines a default value, see Default values section for more details.validate: (value: any) => string | void - defines a custom validator function, see Custom validation section for more details.Usage:
const valueSchema = schema.any();
Notes:
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.schema.maybe()Indicates that input data is optional and may not be present.
Output type: T | undefined
Usage:
const valueSchema = schema.maybe(schema.string());
Notes:
schema.maybe() if a nested type defines a default value.schema.nullable()Indicates that input data is optional and defaults to null if it's not present.
Output type: T | null
Usage:
const valueSchema = schema.nullable(schema.string());
Notes:
schema.nullable() also treats explicitly specified null as a valid input.schema.never()Indicates that input data is forbidden.
Output type: never
Usage:
const valueSchema = schema.never();
Notes:
schema.never() has a very limited application and usually used within conditional schemas to fully or partially forbid input data.schema.uri()Validates input data as a proper URI string (per RFC 3986).
Output type: string
Options:
defaultValue: string | Reference<string> | (() => string) - defines a default value, see Default values section for more details.validate: (value: string) => string | void - defines a custom validator function, see Custom validation section for more details.scheme: string | string[] - limits allowed URI schemes to the one(s) defined here.Usage:
const valueSchema = schema.uri({ scheme: 'https' });
Notes:
schema.uri() for all URI validations even though it may be possible to replicate it with a custom validator for schema.string().schema.byteSize()Validates input data as a proper digital data size.
Output type: ByteSizeValue
Options:
defaultValue: ByteSizeValue | string | number | Reference<ByteSizeValue | string | number> | (() => ByteSizeValue | string | number) - defines a default value, see Default values section for more details.validate: (value: ByteSizeValue | string | number) => string | void - defines a custom validator function, see Custom validation section for more details.min: ByteSizeValue | string | number - defines a minimum value the size should have.max: ByteSizeValue | string | number - defines a maximum value the size should have.Usage:
const valueSchema = schema.byteSize({ min: '3kb' });
Notes:
schema.byteSize() and its options supports the following optional suffixes: b, kb, mb, gb and tb. The default suffix is b.100 is equal to '100b'.0 instead.schema.duration()Validates input data as a proper duration.
Output type: moment.Duration
Options:
defaultValue: moment.Duration | string | number | Reference<moment.Duration | string | number> | (() => moment.Duration | string | number) - defines a default value, see Default values section for more details.validate: (value: moment.Duration | string | number) => string | void - defines a custom validator function, see Custom validation section for more details.Usage:
const valueSchema = schema.duration({ defaultValue: '70ms' });
Notes:
schema.duration() supports the following optional suffixes: ms, s, m, h, d, w, M and y. The default suffix is ms.100 is equal to '100ms'.1m30s).
1m30s1d).1m30s50m is the same as 51m30s).schema.conditional()Allows a specified condition that is evaluated at the validation time and results in either one or another input validation schema.
The first argument is always a reference while the second one can be:
The third argument is a schema that should be used if the result of the aforementioned comparison evaluates to true, otherwise schema.conditional() should fallback
to the schema provided as the fourth argument.
Output type: TTrueResult | TFalseResult
Options:
defaultValue: TTrueResult | TFalseResult | Reference<TTrueResult | TFalseResult> | (() => TTrueResult | TFalseResult - defines a default value, see Default values section for more details.validate: (value: TTrueResult | TFalseResult) => string | void - defines a custom validator function, see Custom validation section for more details.Usage:
const valueSchema = schema.object({
key: schema.oneOf([schema.literal('number'), schema.literal('string')]),
value: schema.conditional(schema.siblingRef('key'), 'number', schema.number(), schema.string()),
});
Notes:
schema.lazy()Allows recursive runtime types to be defined.
Takes a required generic type argument and a required string that represents the id of the schema.
It is recommended to pick a globally unique ID for your schema. Consider creating only IDs that are prefixed with your
domain, e.g. myPlugin_myRecursiveType.
IDs must be unique within a schema ancestry. You can use the same ID in multiple, separate schemas as long as they do not share a common ancestor object. However, if you want to generate OAS from your schema you must ensure a globally unique ID in order to avoid overriding schemas with the same ID.
Note: use of meta.id is required to associate the schema with the ID used in the schema.lazy() call in order to
create a recursive type (see usage).
Output type: T
Usage:
interface RecursiveType {
name: string;
self: undefined | RecursiveType;
}
// Do not assign this ID to any other schema to avoid collisions.
const id = 'myPlugin_myRecursiveType';
const object = schema.object(
{
name: schema.string(),
self: schema.lazy<RecursiveType>(id),
},
{ meta: { id } }
);
Notes:
schema.object() types.schema.contextRef()Defines a reference to the value specified through the validation context. Context reference is only used as part of a conditional schema or as a default value for any other schema.
Output type: TReferenceValue
Usage:
const valueSchema = schema.object({
env: schema.string({ defaultValue: schema.contextRef('envName') }),
});
valueSchema.validate({}, { envName: 'dev' });
Notes:
@kbn/config-schema neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type.environment name that can be used to provide a strict schema for production and more relaxed one for development.schema.siblingRef()Defines a reference to the value of the sibling key. Sibling references are only used a part of conditional schema or as a default value for any other schema.
Output type: TReferenceValue
Usage:
const valueSchema = schema.object({
node: schema.object({ tag: schema.string() }),
env: schema.string({ defaultValue: schema.siblingRef('node.tag') }),
});
Notes:
@kbn/config-schema neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type.Using 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.
For these cases @kbn/config-schema provides a way to specify a custom validation function for almost any schema building block through the validate option.
For 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:
const valueSchema = schema.string({
minLength: 3,
validate(value) {
if (!/^[a-z0-9_-]+$/.test(value)) {
return `must be lower case, a-z, 0-9, '_', and '-' are allowed`;
}
},
});
// ...or if you use that construct a lot...
const regexSchema = (regex: RegExp) => schema.string({
validate: value => regex.test(value) ? undefined : `must match "${regex.toString()}"`,
});
const valueSchema = regexSchema(/^[a-z0-9_-]+$/);
Custom validation function is run only after all built-in validations passed. It should either return a string as an error message
to denote the failed validation or not return anything at all (void) otherwise. Please also note that validate function is synchronous.
Another use case for custom validation functions is when the schema depends on some run-time data:
const gesSchema = randomRunTimeSeed => schema.string({
validate: value => value !== randomRunTimeSeed ? 'value is not allowed' : undefined
});
const schema = gesSchema('some-random-run-time-data');
If 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
deal 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:
const valueSchemaWithPlainValueDefault = schema.string({ defaultValue: 'n/a' });
const valueSchemaWithReferencedValueDefault = schema.string({ defaultValue: schema.contextRef('env') });
const valueSchemaWithFunctionEvaluatedDefault = schema.string({ defaultValue: () => Math.random().toString() });
Notes:
@kbn/config-schema neither validates nor coerces default value and developer is responsible for making sure that it has the appropriate type.It is possible to re-use / extend an existing object schema using the extends API.
The API returns a new instance of schema.object, with a merge of the additional properties
on top of the source object's existing props.
Note that extends only supports extending first-level properties. It's currently not possible to perform deep/nested extensions with a single call.
Example: how to add a new key to an existing object schema
const origin = schema.object({
initial: schema.string(),
});
const extended = origin.extends({
added: schema.number(),
});
Example: How to remove an existing key from an object schema
const origin = schema.object({
initial: schema.string(),
toRemove: schema.number(),
});
const extended = origin.extends({
toRemove: undefined,
});
Example: How to override the schema's options
const origin = schema.object({
initial: schema.string(),
}, { defaultValue: { initial: 'foo' }});
const extended = origin.extends({
added: schema.number(),
}, { defaultValue: { initial: 'foo', added: 'bar' }});