Skip to content

[SavedObjects] Add zod support for schemas and modelVersions#262683

Open
nickofthyme wants to merge 17 commits intoelastic:mainfrom
nickofthyme:zod-model-versions
Open

[SavedObjects] Add zod support for schemas and modelVersions#262683
nickofthyme wants to merge 17 commits intoelastic:mainfrom
nickofthyme:zod-model-versions

Conversation

@nickofthyme
Copy link
Copy Markdown
Contributor

@nickofthyme nickofthyme commented Apr 13, 2026

Summary

Add @kbn/zod support to saved object schemas and modelVersions.*.schemas.

export const mySavedObjectType: SavedObjectsType = {
  name: 'my_saved_object',
  modelVersions: {
    1: {
      changes: [],
      schemas: {
        forwardCompatibility: z.object({...}), // supports zod schemas
        create: z.object({...}), // supports zod schemas
      },
    },
  },
  mappings: {
    // ...some mappings
  },
  schemas: {
    '8.9.0': z.object({...}), // supports zod schemas
  },
};

Note

If you use the SavedObjectsValidationSpec directly to test or validate, you should typeguard for the expected schema using isConfigSchema or isZod before validation.

Checklist

  • Unit or functional tests were updated or added to match the most common scenarios
  • This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The release_note:breaking label should be applied in these situations.
  • The PR description includes the appropriate Release Notes section, and the correct release_note:* label is applied per the guidelines
  • Review the backport guidelines and apply applicable backport:* labels.

@nickofthyme nickofthyme added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting labels Apr 13, 2026
@elastic elastic deleted a comment from elasticmachine Apr 13, 2026
@nickofthyme nickofthyme marked this pull request as ready for review April 13, 2026 18:29
@nickofthyme nickofthyme requested review from a team as code owners April 13, 2026 18:29
Comment on lines -16 to -53
// We convert `SavedObjectSanitizedDoc` to its validation schema representation
// to ensure that we don't forget to keep the schema up-to-date. TS will complain
// if we update `SavedObjectSanitizedDoc` without making changes below.
type SavedObjectSanitizedDocSchema = {
[K in keyof Required<SavedObjectSanitizedDoc>]: Type<SavedObjectSanitizedDoc[K]>;
};

const baseSchema = schema.object<SavedObjectSanitizedDocSchema>({
id: schema.string({ minLength: 1 }),
type: schema.string(),
references: schema.arrayOf(
schema.object({
name: schema.string(),
type: schema.string(),
id: schema.string(),
}),
{ defaultValue: [], maxSize: 1000 }
),
namespace: schema.maybe(schema.string()),
namespaces: schema.maybe(schema.arrayOf(schema.string(), { maxSize: 100 })),
migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())),
coreMigrationVersion: schema.maybe(schema.string()),
typeMigrationVersion: schema.maybe(schema.string()),
updated_at: schema.maybe(schema.string()),
updated_by: schema.maybe(schema.string()),
created_at: schema.maybe(schema.string()),
created_by: schema.maybe(schema.string()),
version: schema.maybe(schema.string()),
originId: schema.maybe(schema.string()),
managed: schema.maybe(schema.boolean()),
accessControl: schema.maybe(
schema.object({
owner: schema.string(),
accessMode: schema.oneOf([schema.literal('write_restricted'), schema.literal('default')]),
})
),
attributes: schema.recordOf(schema.string(), schema.maybe(schema.any())),
});
Copy link
Copy Markdown
Contributor Author

@nickofthyme nickofthyme Apr 13, 2026

Choose a reason for hiding this comment

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

Moved without changes to ./base_schema.ts

Comment on lines +33 to +35
export const createSavedObjectSanitizedDocValidator = (
attributesSchema?: SavedObjectsValidationSpec
): SavedObjectSanitizedDocValidator => {
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.

Now returns generic validator function, no longer the bare schema.

- const schema = createSavedObjectSanitizedDocValidator({...})
+ const validate = createSavedObjectSanitizedDocValidator({...})
- schema.validate(doc)
+ validate(doc)

Comment on lines +10 to +17
/**
* Produces a structure list similar to {@link Type.getSchemaStructure} from `@kbn/config-schema`,
* for Zod v4 schemas (`z` from `@kbn/zod`). Used for saved-object fixture templates and tooling.
*
* @remarks
* This walks Zod's public `def` / `shape` API (Zod v4). Rare wrappers (effects, custom refinements)
* may fall back to a coarse label from `def.type`.
*/
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.

Logic in this file is meant to match the functionality of schema.getSchemaStructure from @kbn/config-schema.

Comment on lines +14 to +26
// We convert `SavedObjectSanitizedDoc` to its validation schema representation
// to ensure that we don't forget to keep the schema up-to-date. TS will complain
// if we update `SavedObjectSanitizedDoc` without making changes below.
type SavedObjectSanitizedDocSchema = {
[K in keyof Required<SavedObjectSanitizedDoc>]: Type<SavedObjectSanitizedDoc[K]>;
};

/**
* Base config-schema schema for a saved object.
*
* @internal
*/
export const baseConfigSchema = schema.object({
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.

One base for each schema type, both validated against SavedObjectSanitizedDoc.

Comment on lines +35 to +37
if (!result.success) {
throw new Error(z.prettifyError(result.error));
}
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.

Return prettified Error instead of ZodError.

Comment on lines +142 to +148
/**
* Zod equivalent of {@link extractMappingCompatibleSchemaFields}. Walks Zod v4 `def` / `shape` (and
* common wrappers) so mapping paths align with the Joi-based extractor for the same logical schema.
*
* @internal Exported for unit tests
*/
export function extractMappingCompatibleZodSchemaFields(
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.

Meant to match the functionality of extractMappingCompatibleSchemaFields for @kbn/config-schema type.

Copy link
Copy Markdown
Contributor

@janmonschke janmonschke left a comment

Choose a reason for hiding this comment

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

kibana-cases changes lgtm (code-review only)

@nickofthyme nickofthyme added backport:version Backport to applied version labels v9.4.0 v9.5.0 and removed backport:skip This PR does not require backporting labels Apr 15, 2026
* @public
*/
export type SavedObjectsValidationSpec = ObjectType;
export type SavedObjectsValidationSpec = ObjectType | z.ZodType<Record<string, unknown>>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SavedObjectsValidationSpec is now a union type, which means consumer tests that call .validate() directly on a validation spec will break at compile time.

The Cases test in this PR shows the migration pattern. Worth calling this out explicitly so consumers know what to expect.

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.

Added note to description, lmk if you want me to call this out in some other way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since the PR changes the SavedObjectsValidationSpec, we need to run the changes by @gsoldevila. I'm concerned there might be some migrations' related gotcha's that could have been missed.

Copy link
Copy Markdown
Contributor

@TinaHeiligers TinaHeiligers left a comment

Choose a reason for hiding this comment

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

This should go in after #263121 (Zod heap fix). Without that patch, every z.object() used in an SO type definition allocates ~12.8 KB of heap. If teams start adopting Zod schemas before the fix is in main, the memory impact adds up quickly.

@elasticmachine
Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] affected Scout: [ security / entity_store ] plugin / local-serverless-security_complete - Entity Store logs extraction broken mapping - should collect new field values on subsequent extractions when entity already exists and field has multi-field sub-fields
  • [job] [logs] affected Scout: [ security / entity_store ] plugin / local-serverless-security_complete - Entity Store logs extraction broken mapping - should extract users successfully when source index has conflicting field mappings (IDP + local id / filters)

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
agentBuilder 1524 1525 +1
agentBuilderPlatform 187 188 +1
alertingVTwo 895 896 +1
apm 2147 2148 +1
automaticImport 270 271 +1
cases 2015 2016 +1
dashboardAgent 620 621 +1
datasetQuality 1077 1078 +1
discover 2019 2020 +1
elasticAssistant 564 565 +1
embeddableAlertsTable 451 452 +1
entityStore 357 358 +1
evals 87 88 +1
fleet 1694 1695 +1
genAiSettings 516 517 +1
logsShared 449 450 +1
ml 4275 4276 +1
observability 1744 1745 +1
observabilityAgentBuilder 105 106 +1
observabilityAIAssistant 174 175 +1
securitySolution 9242 9243 +1
slo 1338 1339 +1
stackConnectors 870 871 +1
streams 239 240 +1
streamsApp 1837 1838 +1
synthetics 1288 1289 +1
timelines 143 144 +1
triggersActionsUi 1448 1449 +1
workflowsExtensions 158 159 +1
workflowsManagement 1595 1596 +1
workplaceAIApp 437 438 +1
total +31

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/zod 6 7 +1

Public APIs missing exports

Total count of every type that is part of your API that should be exported but is not. This will cause broken links in the API documentation system. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats exports for more detailed information.

id before after diff
@kbn/zod 0 1 +1
Unknown metric groups

API count

id before after diff
@kbn/zod 1377 1379 +2

History

@kibanamachine
Copy link
Copy Markdown
Contributor

kibanamachine commented Apr 29, 2026

💔 Build Failed

Failed CI Steps

Test Failures

  • [job] [logs] x-pack/platform/test/serverless/functional/configs/observability/config.group1.ts / Serverless Common UI - Home Page Sample data in serverless Sample data loads

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
agentBuilder 1489 1490 +1
agentBuilderPlatform 187 188 +1
alertingVTwo 934 935 +1
apm 2166 2167 +1
automaticImport 270 271 +1
cases 1973 1974 +1
dashboardAgent 579 580 +1
datasetQuality 1076 1077 +1
discover 2021 2022 +1
elasticAssistant 564 565 +1
embeddableAlertsTable 451 452 +1
entityStore 357 358 +1
evals 87 88 +1
fleet 2112 2113 +1
genAiSettings 516 517 +1
inbox 20 21 +1
logsShared 450 451 +1
ml 4268 4269 +1
observability 1749 1750 +1
observabilityAgentBuilder 105 106 +1
observabilityAIAssistant 174 175 +1
securitySolution 9379 9380 +1
slo 1341 1342 +1
stackConnectors 883 884 +1
streamsApp 1843 1844 +1
synthetics 1295 1296 +1
timelines 143 144 +1
triggersActionsUi 1404 1405 +1
workflowsExtensions 158 159 +1
workflowsManagement 1606 1607 +1
workplaceAIApp 331 332 +1
total +31

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/zod 6 7 +1

Public APIs missing exports

Total count of every type that is part of your API that should be exported but is not. This will cause broken links in the API documentation system. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats exports for more detailed information.

id before after diff
@kbn/zod 0 1 +1
Unknown metric groups

API count

id before after diff
@kbn/zod 1377 1379 +2

History

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:version Backport to applied version labels release_note:skip Skip the PR/issue when compiling release notes v9.4.0 v9.5.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants