[8.18] Add strip unkowns to nested objects in maps, arrays and records (#214978) (#215127)

# Backport

This will backport the following commits from `main` to `8.18`:
- [Add strip unkowns to nested objects in maps, arrays and records
(#214978)](https://github.com/elastic/kibana/pull/214978)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Jesus
Wahrman","email":"41008968+jesuswr@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-03-19T10:38:31Z","message":"Add
strip unkowns to nested objects in maps, arrays and records
(#214978)\n\n## Summary\n\nResolves
https://github.com/elastic/kibana/issues/210617\n\nAdded strip unkowns
to nested objects in map, array and record. Added a\nlot of test cases
to cover things like objects inside maps, objects\ninside records,
objects inside maps inside records, ...\n\nOne thing to note is that we
can't apply `stripUnkowns` to\n`schema.oneOf` since it's using
`joi.alternatives` and you can't use it\nthere.\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"e14369edabf3d4160dc777e3ab190c8a62aeab7e","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Core","release_note:skip","backport:prev-major","kbn/config-schema","backport:current-major","v9.1.0"],"title":"Add
strip unkowns to nested objects in maps, arrays and
records","number":214978,"url":"https://github.com/elastic/kibana/pull/214978","mergeCommit":{"message":"Add
strip unkowns to nested objects in maps, arrays and records
(#214978)\n\n## Summary\n\nResolves
https://github.com/elastic/kibana/issues/210617\n\nAdded strip unkowns
to nested objects in map, array and record. Added a\nlot of test cases
to cover things like objects inside maps, objects\ninside records,
objects inside maps inside records, ...\n\nOne thing to note is that we
can't apply `stripUnkowns` to\n`schema.oneOf` since it's using
`joi.alternatives` and you can't use it\nthere.\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"e14369edabf3d4160dc777e3ab190c8a62aeab7e"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/214978","number":214978,"mergeCommit":{"message":"Add
strip unkowns to nested objects in maps, arrays and records
(#214978)\n\n## Summary\n\nResolves
https://github.com/elastic/kibana/issues/210617\n\nAdded strip unkowns
to nested objects in map, array and record. Added a\nlot of test cases
to cover things like objects inside maps, objects\ninside records,
objects inside maps inside records, ...\n\nOne thing to note is that we
can't apply `stripUnkowns` to\n`schema.oneOf` since it's using
`joi.alternatives` and you can't use it\nthere.\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"e14369edabf3d4160dc777e3ab190c8a62aeab7e"}}]}]
BACKPORT-->

Co-authored-by: Jesus Wahrman <41008968+jesuswr@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2025-03-19 17:43:45 +01:00 committed by GitHub
parent 428edb7fc5
commit b4c5457906
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 568 additions and 30 deletions

View file

@ -227,6 +227,7 @@ __Options:__
* `validate: (value: TValue[]) => string | void` - defines a custom validator function, see [Custom validation](#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:__
```typescript
@ -273,6 +274,7 @@ __Output type:__ `Record<TKey, TValue>`
__Options:__
* `defaultValue: Record<TKey, TValue> | Reference<Record<TKey, TValue>> | (() => Record<TKey, TValue>)` - defines a default value, see [Default values](#default-values) section for more details.
* `validate: (value: Record<TKey, TValue>) => string | void` - defines a custom validator function, see [Custom validation](#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:__
```typescript
@ -292,6 +294,7 @@ __Output type:__ `Map<TKey, TValue>`
__Options:__
* `defaultValue: Map<TKey, TValue> | Reference<Map<TKey, TValue>> | (() => Map<TKey, TValue>)` - defines a default value, see [Default values](#default-values) section for more details.
* `validate: (value: Map<TKey, TValue>) => string | void` - defines a custom validator function, see [Custom validation](#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:__
```typescript
@ -345,6 +348,7 @@ const valueSchema = schema.oneOf([schema.literal('∞'), schema.number()]);
__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.any()`

View file

@ -312,19 +312,25 @@ export const internals: JoiRoot = Joi.extend(
method(key, value) {
return this.$_addRule({ name: 'entries', args: { key, value } });
},
validate(value, { error }, args, options) {
validate(value, { error, prefs }, args, options) {
const result = new Map();
for (const [entryKey, entryValue] of value) {
let validatedEntryKey: any;
try {
validatedEntryKey = Joi.attempt(entryKey, args.key, { presence: 'required' });
validatedEntryKey = Joi.attempt(entryKey, args.key, {
presence: 'required',
stripUnknown: prefs.stripUnknown,
});
} catch (e) {
return error('map.key', { entryKey, reason: e });
}
let validatedEntryValue: any;
try {
validatedEntryValue = Joi.attempt(entryValue, args.value, { presence: 'required' });
validatedEntryValue = Joi.attempt(entryValue, args.value, {
presence: 'required',
stripUnknown: prefs.stripUnknown,
});
} catch (e) {
return error('map.value', { entryKey, reason: e });
}
@ -381,19 +387,25 @@ export const internals: JoiRoot = Joi.extend(
method(key, value) {
return this.$_addRule({ name: 'entries', args: { key, value } });
},
validate(value, { error }, args) {
validate(value, { error, prefs }, args) {
const result = {} as Record<string, any>;
for (const [entryKey, entryValue] of Object.entries(value)) {
let validatedEntryKey: any;
try {
validatedEntryKey = Joi.attempt(entryKey, args.key, { presence: 'required' });
validatedEntryKey = Joi.attempt(entryKey, args.key, {
presence: 'required',
stripUnknown: prefs.stripUnknown,
});
} catch (e) {
return error('record.key', { entryKey, reason: e });
}
let validatedEntryValue: any;
try {
validatedEntryValue = Joi.attempt(entryValue, args.value, { presence: 'required' });
validatedEntryValue = Joi.attempt(entryValue, args.value, {
presence: 'required',
stripUnknown: prefs.stripUnknown,
});
} catch (e) {
return error('record.value', { entryKey, reason: e });
}

View file

@ -216,3 +216,105 @@ describe('#extendsDeep', () => {
).toThrowErrorMatchingInlineSnapshot(`"[0.bar]: definition for this key is missing"`);
});
});
describe('nested unknowns with arrays', () => {
test('should strip unknown nested keys if stripUnkownKeys is true in validate', () => {
const type = schema.arrayOf(
schema.object({
a: schema.string(),
})
);
expect(
type.validate(
[
{ a: '123', b: 'should be stripped' },
{ a: '324', x: 'should be stripped' },
],
void 0,
void 0,
{
stripUnknownKeys: true,
}
)
).toStrictEqual([{ a: '123' }, { a: '324' }]);
});
test('should strip unknown nested keys if unknowns is ignore in the schema', () => {
const type = schema.arrayOf(
schema.object({
a: schema.string(),
}),
{ unknowns: 'ignore' }
);
expect(
type.validate(
[
{ a: '123', b: 'should be stripped' },
{ a: '324', x: 'should be stripped' },
],
void 0,
void 0,
{}
)
).toStrictEqual([{ a: '123' }, { a: '324' }]);
});
test('should strip unknown keys in object inside map inside array when stripUnkownKeys is true', () => {
const type = schema.arrayOf(
schema.mapOf(
schema.string(),
schema.object({
a: schema.string(),
})
)
);
const value = [
new Map([
['key1', { a: '123', b: 'should be stripped' }],
['key2', { a: '456', extra: 'remove this' }],
]),
];
const expected = [
new Map([
['key1', { a: '123' }],
['key2', { a: '456' }],
]),
];
expect(type.validate(value, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
expected
);
});
test('should strip unknown keys in object inside map inside array when unknowns is ignore', () => {
const type = schema.arrayOf(
schema.mapOf(
schema.string(),
schema.object({
a: schema.string(),
})
),
{ unknowns: 'ignore' }
);
const value = [
new Map([
['key1', { a: '123', b: 'should be stripped' }],
['key2', { a: '456', extra: 'remove this' }],
]),
];
const expected = [
new Map([
['key1', { a: '123' }],
['key2', { a: '456' }],
]),
];
expect(type.validate(value, void 0, void 0, {})).toStrictEqual(expected);
});
});

View file

@ -9,12 +9,13 @@
import typeDetect from 'type-detect';
import { internals } from '../internals';
import { Type, TypeOptions, ExtendsDeepOptions } from './type';
import { Type, TypeOptions, ExtendsDeepOptions, UnknownOptions } from './type';
export type ArrayOptions<T> = TypeOptions<T[]> & {
minSize?: number;
maxSize?: number;
};
export type ArrayOptions<T> = TypeOptions<T[]> &
UnknownOptions & {
minSize?: number;
maxSize?: number;
};
export class ArrayType<T> extends Type<T[]> {
private readonly arrayType: Type<T>;
@ -31,6 +32,12 @@ export class ArrayType<T> extends Type<T[]> {
schema = schema.max(options.maxSize);
}
// Only set stripUnknown if we have an explicit value of unknowns
const { unknowns } = options;
if (unknowns) {
schema = schema.options({ stripUnknown: { objects: unknowns === 'ignore' } });
}
super(schema, options);
this.arrayType = type;
this.arrayOptions = options;

View file

@ -223,3 +223,142 @@ describe('#extendsDeep', () => {
});
});
});
describe('nested unknowns', () => {
// we don't allow strip unknowns in oneOf for now because joi
// doesn't allow it in joi.alternatives and we use that for oneOf
test('cant strip unknown keys in oneOf so it should throw an error', () => {
const type = schema.mapOf(
schema.oneOf([schema.literal('a'), schema.literal('b')]),
schema.string()
);
expect(() =>
type.validate(
{
a: 'abc',
x: 'def',
},
void 0,
void 0,
{ stripUnknownKeys: true }
)
).toThrowErrorMatchingInlineSnapshot(`
"[key(\\"x\\")]: types that failed validation:
- [0]: expected value to equal [a]
- [1]: expected value to equal [b]"
`);
});
test('should strip unknown nested keys if stripUnkownKeys is true in validate', () => {
const type = schema.mapOf(
schema.string(),
schema.object({
a: schema.string(),
})
);
const value = {
x: {
a: '123',
b: 'should be stripped',
},
};
const expected = new Map([['x', { a: '123' }]]);
expect(type.validate(value, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
expected
);
});
test('should strip unknown nested keys if unknowns is ignore in the schema', () => {
const type = schema.mapOf(
schema.string(),
schema.object({
a: schema.string(),
}),
{ unknowns: 'ignore' }
);
const value = {
x: {
a: '123',
b: 'should be stripped',
},
};
const expected = new Map([['x', { a: '123' }]]);
expect(type.validate(value, void 0, void 0, {})).toStrictEqual(expected);
});
test('should strip unknown keys in object inside record inside map when stripUnkownKeys is true', () => {
const type = schema.mapOf(
schema.string(),
schema.recordOf(
schema.string(),
schema.object({
a: schema.string(),
})
)
);
const value = new Map([
[
'key1',
{
record1: { a: '123', b: 'should be stripped' },
record2: { a: '456', extra: 'remove this' },
},
],
]);
const expected = new Map([
[
'key1',
{
record1: { a: '123' },
record2: { a: '456' },
},
],
]);
expect(type.validate(value, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
expected
);
});
test('should strip unknown keys in object inside record inside map when unkowns is ignore', () => {
const type = schema.mapOf(
schema.string(),
schema.recordOf(
schema.string(),
schema.object({
a: schema.string(),
})
),
{ unknowns: 'ignore' }
);
const value = new Map([
[
'key1',
{
record1: { a: '123', b: 'should be stripped' },
record2: { a: '456', extra: 'remove this' },
},
],
]);
const expected = new Map([
[
'key1',
{
record1: { a: '123' },
record2: { a: '456' },
},
],
]);
expect(type.validate(value, void 0, void 0, {})).toStrictEqual(expected);
});
});

View file

@ -11,9 +11,9 @@ import typeDetect from 'type-detect';
import { SchemaTypeError, SchemaTypesError } from '../errors';
import { internals } from '../internals';
import { META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES } from '../oas_meta_fields';
import { Type, TypeOptions, ExtendsDeepOptions } from './type';
import { Type, TypeOptions, ExtendsDeepOptions, UnknownOptions } from './type';
export type MapOfOptions<K, V> = TypeOptions<Map<K, V>>;
export type MapOfOptions<K, V> = TypeOptions<Map<K, V>> & UnknownOptions;
export class MapOfType<K, V> extends Type<Map<K, V>> {
private readonly keyType: Type<K>;
@ -22,13 +22,19 @@ export class MapOfType<K, V> extends Type<Map<K, V>> {
constructor(keyType: Type<K>, valueType: Type<V>, options: MapOfOptions<K, V> = {}) {
const defaultValue = options.defaultValue;
const schema = internals
let schema = internals
.map()
.entries(keyType.getSchema(), valueType.getSchema())
.meta({
[META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES]: () => valueType.getSchema(),
});
// Only set stripUnknown if we have an explicit value of unknowns
const { unknowns } = options;
if (unknowns) {
schema = schema.options({ stripUnknown: { objects: unknowns === 'ignore' } });
}
super(schema, {
...options,
// Joi clones default values with `Hoek.clone`, and there is bug in cloning

View file

@ -551,6 +551,127 @@ describe('nested unknowns', () => {
},
});
});
test('should strip unknown keys in object inside record inside map inside object when stripUnkownKeys is true', () => {
const type = schema.object({
rootMap: schema.mapOf(
schema.string(),
schema.recordOf(
schema.string(),
schema.object({
a: schema.string(),
})
)
),
});
const value = {
rootMap: new Map([
[
'key1',
{
record1: { a: '123', b: 'should be stripped' },
record2: { a: '456', extra: 'remove this' },
},
],
]),
anotherKey: 'should also be stripped',
};
const expected = {
rootMap: new Map([
[
'key1',
{
record1: { a: '123' },
record2: { a: '456' },
},
],
]),
};
expect(type.validate(value, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
expected
);
});
});
test('should strip unknown keys in object inside record inside map inside object when unknowns is ignore', () => {
const type = schema.object(
{
rootMap: schema.mapOf(
schema.string(),
schema.recordOf(
schema.string(),
schema.object({
a: schema.string(),
})
)
),
},
{ unknowns: 'ignore' }
);
const value = {
rootMap: new Map([
[
'key1',
{
record1: { a: '123', b: 'should be stripped' },
record2: { a: '456', extra: 'remove this' },
},
],
]),
anotherKey: 'should also be stripped',
};
const expected = {
rootMap: new Map([
[
'key1',
{
record1: { a: '123' },
record2: { a: '456' },
},
],
]),
};
expect(type.validate(value, void 0, void 0, {})).toStrictEqual(expected);
});
test('should strip unknown keys inside schema.oneOf with stripUnknownKeys for both objects at highest level', () => {
const type = schema.oneOf([
schema.object({
a: schema.string(),
}),
schema.object({
b: schema.string(),
}),
]);
const value1 = {
a: 'testA',
c: 'should be stripped',
};
const value2 = {
b: 'testB',
d: 'should be stripped',
};
const expected1 = {
a: 'testA',
};
const expected2 = {
b: 'testB',
};
expect(type.validate(value1, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
expected1
);
expect(type.validate(value2, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
expected2
);
});
});

View file

@ -10,7 +10,7 @@
import type { AnySchema } from 'joi';
import typeDetect from 'type-detect';
import { internals } from '../internals';
import { Type, TypeOptions, ExtendsDeepOptions, OptionsForUnknowns } from './type';
import { Type, TypeOptions, ExtendsDeepOptions, UnknownOptions } from './type';
import { ValidationError } from '../errors';
export type Props = Record<string, Type<any>>;
@ -66,10 +66,6 @@ type ExtendedObjectTypeOptions<P extends Props, NP extends NullableProps> = Obje
ExtendedProps<P, NP>
>;
interface UnknownOptions {
unknowns?: OptionsForUnknowns;
}
interface ObjectTypeOptionsMeta {
/**
* A string that uniquely identifies this schema. Used when generating OAS

View file

@ -63,10 +63,10 @@ test('fails when not receiving expected key type', () => {
};
expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(`
"[key(\\"name\\")]: types that failed validation:
- [0]: expected value to equal [nickName]
- [1]: expected value to equal [lastName]"
`);
"[key(\\"name\\")]: types that failed validation:
- [0]: expected value to equal [nickName]
- [1]: expected value to equal [lastName]"
`);
});
test('fails after parsing when not receiving expected key type', () => {
@ -78,10 +78,10 @@ test('fails after parsing when not receiving expected key type', () => {
const value = `{"name": "foo"}`;
expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(`
"[key(\\"name\\")]: types that failed validation:
- [0]: expected value to equal [nickName]
- [1]: expected value to equal [lastName]"
`);
"[key(\\"name\\")]: types that failed validation:
- [0]: expected value to equal [nickName]
- [1]: expected value to equal [lastName]"
`);
});
test('includes namespace in failure when wrong top-level type', () => {
@ -211,6 +211,147 @@ describe('#extendsDeep', () => {
});
});
describe('nested unknowns', () => {
// we don't allow strip unknowns in oneOf for now because joi
// doesn't allow it in joi.alternatives and we use that for oneOf
test('cant strip unknown keys in oneOf so it should throw an error', () => {
const type = schema.recordOf(
schema.oneOf([schema.literal('a'), schema.literal('b')]),
schema.string()
);
expect(() =>
type.validate(
{
a: 'abc',
x: 'def',
},
void 0,
void 0,
{ stripUnknownKeys: true }
)
).toThrowErrorMatchingInlineSnapshot(`
"[key(\\"x\\")]: types that failed validation:
- [0]: expected value to equal [a]
- [1]: expected value to equal [b]"
`);
});
test('should strip unknown nested keys if stripUnkownKeys is true in validate', () => {
const type = schema.recordOf(
schema.string(),
schema.object({
a: schema.string(),
})
);
expect(
type.validate(
{
x: {
a: '123',
b: 'should be stripped',
},
},
void 0,
void 0,
{ stripUnknownKeys: true }
)
).toStrictEqual({
x: {
a: '123',
},
});
});
test('should strip unknown nested keys if unknowns is ignore in the schema', () => {
const type = schema.recordOf(
schema.string(),
schema.object({
a: schema.string(),
}),
{ unknowns: 'ignore' }
);
expect(
type.validate(
{
x: {
a: '123',
b: 'should be stripped',
},
},
void 0,
void 0,
{}
)
).toStrictEqual({
x: {
a: '123',
},
});
});
test('should strip unknown keys in object inside map inside record when stripUnkownKeys is true', () => {
const type = schema.recordOf(
schema.string(),
schema.mapOf(
schema.string(),
schema.object({
a: schema.string(),
})
)
);
const value = {
record1: new Map([
['key1', { a: '123', b: 'should be stripped' }],
['key2', { a: '456', extra: 'remove this' }],
]),
};
const expected = {
record1: new Map([
['key1', { a: '123' }],
['key2', { a: '456' }],
]),
};
expect(type.validate(value, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
expected
);
});
test('should strip unknown keys in object inside map inside record when unkowns is ignore', () => {
const type = schema.recordOf(
schema.string(),
schema.mapOf(
schema.string(),
schema.object({
a: schema.string(),
})
),
{ unknowns: 'ignore' }
);
const value = {
record1: new Map([
['key1', { a: '123', b: 'should be stripped' }],
['key2', { a: '456', extra: 'remove this' }],
]),
};
const expected = {
record1: new Map([
['key1', { a: '123' }],
['key2', { a: '456' }],
]),
};
expect(type.validate(value, void 0, void 0, {})).toStrictEqual(expected);
});
});
test('meta', () => {
const stringSchema = schema.string();
const type = schema.mapOf(schema.string(), stringSchema);

View file

@ -10,10 +10,10 @@
import typeDetect from 'type-detect';
import { SchemaTypeError, SchemaTypesError } from '../errors';
import { internals } from '../internals';
import { Type, TypeOptions, ExtendsDeepOptions } from './type';
import { Type, TypeOptions, ExtendsDeepOptions, UnknownOptions } from './type';
import { META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES } from '../oas_meta_fields';
export type RecordOfOptions<K extends string, V> = TypeOptions<Record<K, V>>;
export type RecordOfOptions<K extends string, V> = TypeOptions<Record<K, V>> & UnknownOptions;
export class RecordOfType<K extends string, V> extends Type<Record<K, V>> {
private readonly keyType: Type<K>;
@ -21,13 +21,19 @@ export class RecordOfType<K extends string, V> extends Type<Record<K, V>> {
private readonly options: RecordOfOptions<K, V>;
constructor(keyType: Type<K>, valueType: Type<V>, options: RecordOfOptions<K, V> = {}) {
const schema = internals
let schema = internals
.record()
.entries(keyType.getSchema(), valueType.getSchema())
.meta({
[META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES]: () => valueType.getSchema(),
});
// Only set stripUnknown if we have an explicit value of unknowns
const { unknowns } = options;
if (unknowns) {
schema = schema.options({ stripUnknown: { objects: unknowns === 'ignore' } });
}
super(schema, options);
this.keyType = keyType;
this.valueType = valueType;

View file

@ -69,6 +69,10 @@ export interface SchemaValidationOptions {
*/
export type OptionsForUnknowns = 'allow' | 'ignore' | 'forbid';
export interface UnknownOptions {
unknowns?: OptionsForUnknowns;
}
export interface ExtendsDeepOptions {
unknowns?: OptionsForUnknowns;
}