Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f2a1156
docs, thanks Claude!
jloleysens Jan 13, 2026
f0c1bd7
very early first pass
jloleysens Jan 13, 2026
f7ff49f
handle catch all
jloleysens Jan 14, 2026
f006362
update discriminated union type
jloleysens Jan 14, 2026
50b2eb9
remove unused code
jloleysens Jan 14, 2026
7a5d137
support parsing the special discriminator type and add tests
jloleysens Jan 14, 2026
6ff5530
minor update to shared types
jloleysens Jan 14, 2026
bce8d7b
actually pass down the shared schemas
jloleysens Jan 14, 2026
4896447
added some high level tests
jloleysens Jan 14, 2026
225c7a1
make sure that you are looking at a discriminator schema
jloleysens Jan 15, 2026
6dad676
Merge branch 'main' into oas/generate-discriminator
jloleysens Jan 15, 2026
ed707a9
only the latest version
jloleysens Jan 16, 2026
1f0eb63
Merge branch 'main' into oas/generate-discriminator
jloleysens Jan 16, 2026
9a2d577
comment
jloleysens Jan 16, 2026
af4302f
Merge branch 'main' into oas/generate-discriminator
jloleysens Jan 19, 2026
dc1315d
Merge branch 'main' into oas/generate-discriminator
jloleysens Jan 21, 2026
0368ec3
Merge branch 'main' into oas/generate-discriminator
jloleysens Jan 21, 2026
c4b8755
Merge branch 'main' into oas/generate-discriminator
jloleysens Jan 21, 2026
62dc8a7
Merge branch 'main' into oas/generate-discriminator
jloleysens Jan 23, 2026
4f47aa4
catch all -> default case and restructure tests for more clarity
jloleysens Jan 23, 2026
54f87dd
clean up description and update test snapshots
jloleysens Jan 23, 2026
190cda8
it -> test
jloleysens Jan 23, 2026
49f15c6
Merge branch 'main' into oas/generate-discriminator
jloleysens Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/core/server/integration_tests/http/oas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,7 @@ it.each([
],
requestBody: {
content: {
'application/json; Elastic-Api-Version=1': {}, // Multiple body types
'application/json; Elastic-Api-Version=2': {},
'application/json; Elastic-Api-Version=2': {}, // Only the latest version
},
},
},
Expand Down
100 changes: 67 additions & 33 deletions src/platform/packages/shared/kbn-config-schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,41 @@ Kibana configuration entries providing developers with a fully typed model of th

## Table of Contents

* [Why `@kbn/config-schema`?](#why-kbnconfig-schema)
* [Base concepts](#base-concepts)
* [Basic types](#basic-types)
* [`schema.string()`](#schemastring)
* [`schema.number()`](#schemanumber)
* [`schema.boolean()`](#schemaboolean)
* [`schema.literal()`](#schemaliteral)
* [`schema.buffer()`](#schemabuffer)
* [`schema.stream()`](#schemastream)
* [Composite types](#composite-types)
* [`schema.arrayOf()`](#schemaarrayof)
* [`schema.object()`](#schemaobject)
* [`schema.recordOf()`](#schemarecordof)
* [`schema.mapOf()`](#schemamapof)
* [`schema.intersection()` / `schema.allOf()`](#schemaintersection--schemaallof)
* [Advanced types](#advanced-types)
* [`schema.oneOf()`](#schemaoneof)
* [`schema.any()`](#schemaany)
* [`schema.maybe()`](#schemamaybe)
* [`schema.nullable()`](#schemanullable)
* [`schema.never()`](#schemanever)
* [`schema.uri()`](#schemauri)
* [`schema.byteSize()`](#schemabytesize)
* [`schema.duration()`](#schemaduration)
* [`schema.conditional()`](#schemaconditional)
* [`schema.lazy()`](#schemalazy)
* [References](#references)
* [`schema.contextRef()`](#schemacontextref)
* [`schema.siblingRef()`](#schemasiblingref)
* [Custom validation](#custom-validation)
* [Default values](#default-values)
* [Extending object schemas](#extending-object-schemas)
- [`@kbn/config-schema` — The Kibana config validation library](#kbnconfig-schema--the-kibana-config-validation-library)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, just adding missing README entry 🧹

- [Table of Contents](#table-of-contents)
- [Why `@kbn/config-schema`?](#why-kbnconfig-schema)
- [Base concepts](#base-concepts)
- [Basic types](#basic-types)
- [`schema.string()`](#schemastring)
- [`schema.number()`](#schemanumber)
- [`schema.boolean()`](#schemaboolean)
- [`schema.literal()`](#schemaliteral)
- [`schema.buffer()`](#schemabuffer)
- [`schema.stream()`](#schemastream)
- [Composite types](#composite-types)
- [`schema.arrayOf()`](#schemaarrayof)
- [`schema.object()`](#schemaobject)
- [`schema.recordOf()`](#schemarecordof)
- [`schema.mapOf()`](#schemamapof)
- [`schema.intersection()` / `schema.allOf()`](#schemaintersection--schemaallof)
- [Advanced types](#advanced-types)
- [`schema.oneOf()`](#schemaoneof)
- [`schema.discriminatedUnion()`](#schemadiscriminatedunion)
- [`schema.any()`](#schemaany)
- [`schema.maybe()`](#schemamaybe)
- [`schema.nullable()`](#schemanullable)
- [`schema.never()`](#schemanever)
- [`schema.uri()`](#schemauri)
- [`schema.byteSize()`](#schemabytesize)
- [`schema.duration()`](#schemaduration)
- [`schema.conditional()`](#schemaconditional)
- [`schema.lazy()`](#schemalazy)
- [References](#references)
- [`schema.contextRef()`](#schemacontextref)
- [`schema.siblingRef()`](#schemasiblingref)
- [Custom validation](#custom-validation)
- [Default values](#default-values)
- [Extending object schemas](#extending-object-schemas)

## Why `@kbn/config-schema`?

Expand Down Expand Up @@ -307,7 +310,7 @@ __Notes:__

### `schema.intersection()` / `schema.allOf()`

Creates an `object` schema being the intersection of the provided `object` schemas.
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](#schemaobject).
Expand Down Expand Up @@ -350,6 +353,37 @@ __Notes:__
* Since the result data type is a type union you should use various TypeScript type guards to get the exact type.
* Can't use the `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](#default-values) section for more details.
* `validate: (value: TUnion) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details.

__Usage:__
```typescript
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:__
* The first argument is the name of the discriminator property that must be present in all object schemas.
* Each object schema defines the discriminator property using `schema.literal()` with a unique string value.
* Discriminator values must be unique across all schemas - duplicate values will throw an error at schema construction time.
* You can define a fallback schema by using a non-literal type (e.g., `schema.string()`) for the discriminator property. Only one fallback schema is allowed.
* Unlike `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.
Expand Down
4 changes: 4 additions & 0 deletions src/platform/packages/shared/kbn-config-schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,8 @@ import {
META_FIELD_X_OAS_MAX_LENGTH,
META_FIELD_X_OAS_MIN_LENGTH,
META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES,
META_FIELD_X_OAS_DISCRIMINATOR,
META_FIELD_X_OAS_DISCRIMINATOR_DEFAULT_CASE,
} from './src/oas_meta_fields';

export const metaFields = Object.freeze({
Expand All @@ -693,4 +695,6 @@ export const metaFields = Object.freeze({
META_FIELD_X_OAS_MAX_LENGTH,
META_FIELD_X_OAS_MIN_LENGTH,
META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES,
META_FIELD_X_OAS_DISCRIMINATOR,
META_FIELD_X_OAS_DISCRIMINATOR_DEFAULT_CASE,
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ export const META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES =
'x-oas-get-additional-properties' as const;
export const META_FIELD_X_OAS_DEPRECATED = 'x-oas-deprecated' as const;
export const META_FIELD_X_OAS_DISCRIMINATOR = 'x-oas-discriminator' as const;
/** An internal meta field used to indicate that the "default" special case handler in a discriminator */
export const META_FIELD_X_OAS_DISCRIMINATOR_DEFAULT_CASE =
'x-oas-discriminator-default-case' as const;
export const META_FIELD_X_OAS_ANY = 'x-oas-any-type' as const;
export const META_FIELD_X_OAS_DISCONTINUED = 'x-oas-discontinued' as const;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import type { Schema, SwitchCases } from 'joi';
import typeDetect from 'type-detect';

import { internals } from '../internals';
import { META_FIELD_X_OAS_DISCRIMINATOR } from '../oas_meta_fields';
import {
META_FIELD_X_OAS_DISCRIMINATOR,
META_FIELD_X_OAS_DISCRIMINATOR_DEFAULT_CASE,
} from '../oas_meta_fields';
import type { ExtendsDeepOptions } from './type';
import { Type } from './type';
import type { ObjectResultType, Props } from './object_type';
Expand Down Expand Up @@ -50,7 +53,7 @@ export class DiscriminatedUnionType<
throw new Error(`Only one fallback schema is allowed`);
}

otherwise = type.getSchema();
otherwise = type.getSchema().meta({ [META_FIELD_X_OAS_DISCRIMINATOR_DEFAULT_CASE]: true });
return acc;
} else {
if (typeof discriminatorValue !== 'string') {
Expand All @@ -76,9 +79,19 @@ export class DiscriminatedUnionType<
return acc;
}, []);

let schema = internals
// This is a workaround to add the discriminator to the first case because our parser
// strips it off the alternatives.match container.
// https://github.com/kenspirit/joi-to-json/pull/58
if (switchCases.length > 0) {
switchCases[0].then = (switchCases[0]!.then! as Schema).meta({
[META_FIELD_X_OAS_DISCRIMINATOR]: discriminator,
});
}

const schema = internals
.alternatives()
.match('any')
.meta({ [META_FIELD_X_OAS_DISCRIMINATOR]: discriminator })
.conditional(
internals.ref(`.${discriminator}`), // self reference object property
{
Expand All @@ -87,8 +100,6 @@ export class DiscriminatedUnionType<
}
);

schema = schema.meta({ [META_FIELD_X_OAS_DISCRIMINATOR]: discriminator });

super(schema, options);

this.discriminator = discriminator;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading