Skip to content

[Cases] Replace io-ts with zod#268342

Open
lgestc wants to merge 44 commits into
elastic:mainfrom
lgestc:tasks/replace-io-ts-in-cases-plugin
Open

[Cases] Replace io-ts with zod#268342
lgestc wants to merge 44 commits into
elastic:mainfrom
lgestc:tasks/replace-io-ts-in-cases-plugin

Conversation

@lgestc
Copy link
Copy Markdown
Contributor

@lgestc lgestc commented May 8, 2026

Summary

Replaces the io-ts-based runtime validation layer in the Cases plugin with zod, consolidating the multi-PR migration tracked through #258427 and the subsequent staged PRs (PR 23, PR 24aPR 24d). Every *Rt codec under common/types/api, common/types/domain, and common/schema is rewritten in zod (*Schema); decodeOrThrow / decodeWithExcessOrThrow route call sites become decodeOrThrowZod / decodeWithExcessOrThrowZod; io-ts is removed from the plugin's runtime imports entirely, and the @kbn/securitysolution-io-ts-utils / io-ts / fp-ts deps drop out of tsconfig.json. All publicly exported TypeScript types (Case, Cases, CasePostRequest, CaseConnector, AttachmentType, CaseSeverity, …) are preserved through z.infer<…> so no external consumer is affected.

Motivation

io-ts is in maintenance mode upstream and the Cases plugin was the last large io-ts consumer in x-pack/platform/plugins/shared/. Moving to zod aligns Cases with the rest of Kibana (route validation, @kbn/zod, @kbn/zod-helpers, OpenAPI-generated bundled types), unblocks future schema work that depends on zod-only features (e.g. DeepStrict, the OpenAPI generator pipeline already used in common/bundled-types.gen.ts), and removes a transitive fp-ts runtime dependency from the plugin.

Changes

  • Schema rewrite: every io-ts codec (*Rt) under common/types/{api,domain} and common/schema is rewritten as a zod schema (*Schema). The parallel scaffolding under common/types/api_zod / common/types/domain_zod from the earlier staging PRs is folded back into the canonical paths and removed.
  • Decode helpers: server/common/runtime_types.ts now exposes decodeOrThrowZod (strip-on-unknown, matching rt.exact) and decodeWithExcessOrThrowZod (uses DeepStrict from @kbn/zod-helpers to reject unknown keys at any depth, matching io-ts exactCheck). All route handlers, client methods, and SO migrations are switched over.
  • Validator parity: three behavioral regressions surfaced while porting the test suite were fixed and locked: NonEmptyString rejects whitespace-only strings (s.trim().length >= 1), paginationSchema rejects non-numeric page/perPage strings (NumberFromString parity, replaces silent NaN), and limitedStringSchema accepts empty / whitespace-only input only when min === 0.
  • Test coverage restored and extended: ports the deleted common/schema/index.test.ts to a zod-native suite; restores the four common/types/domain/template/* business-logic tests (evaluate_conditions, fields, validate_extended_fields, v1) that were swept up in the codec cleanup but cover pure functions independent of io-ts; adds new unit suites for the top-level request schemas (CasePostRequestSchema, CasePatchRequestSchema, CasesPatchRequestSchema, CasesFindRequestSchema, CasesSearchRequestSchema, AttachmentRequestSchema, AttachmentPatchRequestSchema, bulk attachment requests); adds DeepStrict regression tests covering excess keys inside arrays of objects, union variants, and discriminated-union variants.
  • Documentation: restores ~100 field-level jsdocs and section headers across 12 schema files that were dropped during the codec rewrite, including the load-bearing rationales (the extended_fields snake_case key justification anchored to getUserActionItemByDifference, the AttachmentPatchRequestSchema partial-update warning anchored to injectAttachmentSOAttributesFromRefsForPatch, the @deprecated pointer on CaseUserActionInjectedDeprecatedIdsSchema, and the UserActionTypes "do not remove" preamble).

Risk & Migration

Score: 3 — Wholesale validation-layer rewrite isolated to the Cases plugin. No public TypeScript types change shape (preserved via z.infer), no SO schema or RBAC surface is touched, and there are no external consumers of the removed io-ts codec exports (verified by monorepo grep across x-pack/solutions/, x-pack/plugins/, src/). Backward compatible at the API layer: every error message that downstream tooling/integrations might rely on ("The X field cannot be an empty string.", "cannot parse to a number", "The provided perPage value is too high. …", "The length of the field X is too long. Array must be of length <= N.") is preserved verbatim under zod via superRefine.

Testing

Manual:

  1. Boot Kibana (yarn start --no-base-path or your usual flow) and log in.
  2. Stack Management → Cases: create a case with a valid title and description. Confirm it persists and the detail page renders.
  3. On the same case, post a comment and confirm it renders in the timeline.
  4. Try submitting the create-case form with a whitespace-only name — the form should reject client-side with "A name is required."
  5. Hit the API directly to verify server-side parity:
    • curl -u elastic:<pw> -H "kbn-xsrf: true" -H "Content-Type: application/json" -X POST http://localhost:5601/api/cases -d '{"title":" ","description":"d","tags":[],"connector":{"id":"none","name":"none","type":".none","fields":null},"settings":{"syncAlerts":false},"owner":"cases"}' → expect 400 "title: The title field cannot be an empty string."
    • curl -u elastic:<pw> "http://localhost:5601/api/cases/_find?page=abc" → expect 400 "page: cannot parse to a number"
    • curl -u elastic:<pw> "http://localhost:5601/api/cases/_find?perPage=999" → expect 400 "The provided perPage value is too high. …"
  6. Smoke-tested locally end-to-end with playwright + curl on a running 9.x stack — happy path and all three parity error messages confirmed.

Automated:

  • Unit tests added/updated:
    • x-pack/platform/plugins/shared/cases/common/schema/index.test.ts (zod-native port, 38 cases)
    • x-pack/platform/plugins/shared/cases/common/types/api/case/v1.test.ts (43 cases)
    • x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.test.ts (37 cases)
    • x-pack/platform/plugins/shared/cases/server/common/runtime_types.test.ts (DeepStrict parity at depth)
    • x-pack/platform/plugins/shared/cases/common/types/domain/template/{evaluate_conditions,fields,validate_extended_fields,v1}.test.ts (103 cases, restored)
    • all per-route handler tests in server/routes/api/**, server/client/cases/**, server/services/**, server/saved_object_types/migrations/** updated for the zod helper switch
  • Integration tests: existing x-pack/platform/test/cases_api_integration/ suite covers route-level wiring; no new tests were needed there since the route contract didn't change.
  • Full plugin Jest suite: 7286 / 7286 passed (excluding the pre-existing flaky / SIGSEGV cases that pass when re-run isolated).

Checklist

Check the PR satisfies following conditions.
Reviewers should verify this PR satisfies this list as well.

  • Any text added follows EUI's writing guidelines, uses sentence case text and includes i18n support
  • Documentation was added for features that require explanation or tutorials
  • Unit or functional tests were updated or added to match the most common scenarios
  • If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the docker list
  • 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.
  • Flaky Test Runner was used on any tests changed
  • 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.

lgestc and others added 30 commits May 7, 2026 16:46
Swap decodeOrThrow / decodeWithExcessOrThrow calls in the configure
sub-client to their zod equivalents. Casts are localized to client.ts
where zod-inferred literal-union severity types collide with io-ts
CaseSeverity at the public interface boundary. Tests updated to match
zod's error message format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap decoders in get/find/search/similar/bulk_get/observables to their
zod equivalents. Cast at decoder return sites to bridge zod-inferred
literal-union enum types and io-ts TypeScript enums at the public
interface boundary.

Also fixes a stage-1 parity gap in the zod paginationSchema, which was
missing the maxPerPage and MAX_DOCS_PER_PAGE refinements present in the
io-ts version. Tests updated to match zod's error message format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…R 6)

Swap decoders in create/bulk_create/bulk_update/push/delete/replace_custom_field
to their zod equivalents. Cast at decoder return sites to bridge zod-inferred
literal-union enum types and io-ts TypeScript enums at the public interface
boundary.

Also fixes two stage-1 parity gaps:
- Adds CaseCloseReasonSchema to domain_zod and threads closeReason into
  CaseBaseOptionalFieldsRequestSchema in api_zod (was present in io-ts).

Tests updated to match zod's error message format (path-prefixed error
messages, `Excess keys are not allowed`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap decoders in user_actions/{get,find,stats,users,connectors} to their
zod equivalents.

Adds two missing schemas to api_zod/user_action (parity gap from stage 1):
- CaseUserActionDeprecatedResponseSchema
- CaseUserActionsDeprecatedResponseSchema

One test updated to match zod's `Excess keys are not allowed` message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap decoders in attachments/{add,add_file,bulk_create,bulk_delete,
bulk_get,delete,get,update} to their zod equivalents.

Adds three missing schemas to api_zod / domain_zod attachment v2
(parity gap from stage 1):
- AttachmentsSchemaV2 (array)
- AttachmentsFindResponseSchemaV2
- BulkGetAttachmentsResponseSchemaV2

Tests updated to match zod's error message format (path-prefixed
messages, `Excess keys are not allowed`, distinct
`Invalid input: expected …, received …` for missing fields).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap decoders in metrics/{get_case_metrics,get_cases_metrics,
get_status_totals} and workflows/steps/update_case_helpers to their zod
equivalents.

Three test assertions updated to match zod's error message format
(`Excess keys are not allowed`, `Invalid input: expected …`).

Note: metrics/lifespan.ts still uses io-ts `StatusUserActionRt.is()` as
a runtime type guard — that's a different pattern (not a decoder) and
is left for a later cleanup PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap decoders in routes/api/configure/{post_configure,patch_configure}
to their zod equivalents. These are the only routes that call
decodeOrThrow / decodeWithExcessOrThrow directly — other routes
delegate validation to the cases client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace io-ts in server-side helper type files and their consumers:

- common/types/case.ts — CaseTransformedAttributesSchema,
  getPartialCaseTransformedAttributesSchema, OwnerSchema (was *Rt)
- common/types/configure.ts — ConfigurationPartialAttributesSchema,
  ConfigurationTransformedAttributesSchema (was *Rt)
- common/types/connector_mappings.ts —
  ConnectorMappingsAttributesTransformedSchema,
  ConnectorMappingsAttributesPartialSchema (was *Rt)
- common/types/user_actions.ts — UserActionTransformedAttributesSchema,
  UserActionPersistedAttributesSchema (was *Rt)
- services/utils.ts — bulkDecodeSOAttributes now takes a ZodType

Consumers updated: services/cases/index.ts, services/configure/index.ts,
services/connector_mappings/index.ts, services/user_actions/index.ts,
services/user_actions/operations/{find,create}.ts. Casts at decoder
return sites bridge zod-inferred types and io-ts types where they
diverge (enum identity, union variant counts).

Test assertions in the affected suites updated to match zod's error
message format (path-prefixed messages, broader `Invalid` substring
where the discriminator-vs-Invalid-input distinction varies).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (PR 12)

Replace io-ts in three files:

- attachment_framework/attachments/comment.ts — CommentAttachmentDataRt
  → CommentAttachmentDataSchema (zod). Validator behavior unchanged.
- internal_attachments/index.ts — file/lens schema validators swapped
  to zod (FileAttachmentMetadataSchema, jsonValueSchema, ad-hoc
  LensAttachmentDataSchema).
- saved_object_types/migrations/user_actions/connector_id.ts — io-ts
  type guards (`CaseConnectorRt.is`, `ExternalServiceRt.is`) replaced
  with `Schema.safeParse(...).success`. The `actionDetails is X` user-
  type-guard return signatures preserved by importing the corresponding
  io-ts-derived `CaseConnector` / `ExternalService` types.

These validators are part of the cases plugin's attachment-framework
contract used by Security Solution and others. The migration is a
type-system refactor (no runtime-behavior change) but the public API
surface for plugins registering attachment types changes from io-ts
codecs to zod schemas. **Coordinate with stakeholders before merging.**

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace `CaseFileMetadataForDeletionRt` with
`CaseFileMetadataForDeletionSchema` (zod) and update the only live
consumer (`server/client/attachments/bulk_delete.ts`), which used the
io-ts `.is()` type-guard. Replaced with `.safeParse(...).success` and
the now-typed `parsed.data` is used directly to narrow the meta access.

The other common helpers in this PR's plan
(`common/api/runtime_types.ts`, `common/api/saved_object.ts`,
`common/schema/{index,types}.ts`) are only consumed by io-ts files
scheduled for deletion in PR 15, so they're left as-is and will go
away with that cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace io-ts in five public-side files:

- public/components/all_cases/schema.ts — AllCasesURLQueryParamsRt
  → AllCasesURLQueryParamsSchema (zod). validateSchema rewritten to
  use safeParse.
- public/components/all_cases/types.ts — AllCasesURLQueryParams now
  inferred from the zod schema.
- public/components/all_cases/utils/parse_url_params.tsx — consumer
  updated to the new schema name.
- public/components/attachments/file/types.ts — DownloadableFile
  inferred from SingleFileAttachmentMetadataSchema.
- public/components/attachments/{comment,lens}/index.tsx — local
  schema validators converted to zod (safeParse(...).success).

Test (schema.test.ts) rewritten against the zod API; all other tests
in the affected directories pass without changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace io-ts decoders in public/api/decoders.ts:
- CasesFindResponseRt → CasesFindResponseSchema
- CasesBulkGetResponseRt → CasesBulkGetResponseSchema
- CasesMetricsResponseRt → CasesMetricsResponseSchema
- CasesSimilarResponseRt → CasesSimilarResponseSchema

Replaces the fp-ts pipe(decode, fold(throwErrors, identity)) chain with
a local decodeWithToasterError helper (same pattern as
public/containers/utils.ts). Uses safeParse and surfaces validation
errors via the existing ToasterError class.

The decoder function signatures keep the io-ts-typed Cases*Response
parameter and use NonNullable<T> for the return so callers see the
narrowed type, matching the contract from PR 1+2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace `FileAttachmentMetadataRt.is(...)` with the zod equivalent in
public/components/attachments/file/utils.tsx. Restructured
isValidFileExternalReferenceMetadata to use the parsed result for the
.length check (zod's safeParse(...).success doesn't narrow inline like
io-ts's .is() does).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace io-ts decoders/types in:
- server/common/types/attachments_v1.ts — *Rt → *Schema
- server/services/attachments/index.ts — many decodeOrThrow swaps,
  casts to AttachmentPatchAttributesV2 at decoder return sites
- server/services/attachments/operations/get.ts — V2 + legacy decoders
- server/services/attachments/operations/utils.ts — UnifiedAttachment*

Also fills two stage-1 parity gaps in domain_zod/attachment/v2:
- Adds AttachmentPatchAttributesSchemaV2 (mirrors io-ts
  AttachmentPatchAttributesRtV2)
- Adds DocumentAttachmentAttributesSchemaV2 with the
  UnifiedEventDocumentAttachment* internal pieces

Snapshots updated for zod's error format (path-prefixed messages,
Excess keys are not allowed). All 79 attachments service tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…R 18)

Replace `*Rt.is(...)` with `*Schema.safeParse(...).success` in
server/services/user_actions/type_guards.ts:
- CaseAssigneesRt → CaseAssigneesSchema
- CaseCustomFieldsRt → CaseCustomFieldsSchema
- CaseSettingsRt → CaseSettingsSchema
- ExtendedFieldsRt → ExtendedFieldsSchema

The user-defined type-guard return signatures are preserved by keeping
the io-ts-derived parameter types (CaseAssignees, CaseCustomFields,
CaseSettings) imported from common/types/domain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace SuggestUserProfilesRequestRt with SuggestUserProfilesRequestSchema
and decodeWithExcessOrThrow with decodeWithExcessOrThrowZod in
server/services/user_profiles/index.ts.

Test assertion updated to match zod's path-prefixed error format
(`size: The size field cannot be more than 10.`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace io-ts decoders with zod equivalents in 4 route handlers:
- routes/api/cases/push_case.ts — CasePushRequestParamsRt → Schema
- routes/api/comments/patch_comment.ts — AttachmentPatchRequestRt
  with cast to AttachmentPatchRequestV2 to satisfy the io-ts-typed
  attachments.update payload
- routes/api/internal/bulk_get_attachments.ts — BulkGetAttachmentsRequestRt
- routes/api/internal/bulk_delete_file_attachments.ts —
  BulkDeleteFileAttachmentsRequestRt

These are the only routes that called decode* directly; other routes
delegate validation to the cases client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace io-ts in server/common helper files:
- server/common/utils.ts — `ExternalReferenceSOAttachmentPayloadRt.is(...)`
  and `FileAttachmentMetadataRt.is(...)` swapped to safeParse. The two
  guards are chained via parsed.data so the second check sees the
  narrowed metadata field.
- server/common/models/case_with_comments.ts — CaseRt decoder swapped
  to CaseSchema with cast to Case at return site.
- server/common/types/id_incrementer.ts —
  CaseIdIncrementerTransformedAttributesRt → *Schema. No external
  consumers for this re-export.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace io-ts payload decoders with zod equivalents in
server/client/utils.ts. Seven discriminator-payload decoders swapped:
ActionsAttachmentPayload, AlertAttachmentPayload, EventAttachmentPayload,
ExternalReferenceNoSOAttachmentPayload, ExternalReferenceSOAttachmentPayload,
PersistableStateAttachmentPayload, UserCommentAttachmentPayload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace `StatusUserActionRt.is(...)` with
`StatusUserActionSchema.safeParse(...).success` in
server/client/metrics/lifespan.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Additive prep for the io-ts → zod rename. Adds type aliases that the
io-ts files exported but the zod mirror files didn't, so consumers
will keep working when the api/domain dirs become zod-only:

api_zod:
- case/v1.ts — AllCategoriesFindRequest, AllReportersFindRequest,
  AllTagsFindRequest, CaseRequestCustomField, CaseRequestCustomFields,
  CasesFindRequestSortFields, CasesFindRequestWithCustomFields,
  CasesSearchRequest, CasesSimilarResponse,
  FindCasesContainingAllAlertsResponse,
  FindCasesContainingAllDocumentsRequest,
  GetRelatedCasesByAlertResponse, SimilarCasesSearchRequest
- configure/v1.ts — GetConfigureResponse, CreateConfigureResponse,
  UpdateConfigureResponse
- connector/v1.ts — GetCaseConnectorsPushDetails
- metrics/v1.ts — AlertHostsMetrics, AlertUsersMetrics, StatusInfo,
  SingleCaseMetricsFeatureField, CasesMetricsFeatureField
- user_action/v1.ts — UserActionFindRequestTypes, UserActionWithResponse<T>

domain_zod:
- connector/v1.ts — inlines ConnectorTypes/SwimlaneConnectorType enums
  and ActionConnector/ActionTypeConnector aliases (previously
  re-exported from io-ts); adds Connector{CasesWebhook,Jira,Resilient,
  ServiceNowITSM,ServiceNowSIR,Swimlane,TheHive}TypeFields aliases
- user_action/action/v1.ts — inlines UserActionTypes/UserActionActions
  enums and UserActionType/UserActionAction aliases (previously
  re-exported from io-ts)
- user_action/v1.ts — imports missing TemplateUserActionSchema,
  ExtendedFieldsUserActionSchema, CommentUserActionPayloadWithoutIdsSchema,
  SettingsUserActionPayloadSchema; adds the *UserAction discriminated-
  union type aliases plus UserAction<T>, UserActionWithAttributes<T>,
  UserActionWithDeprecatedResponse<T> generics
- attachment/v2.ts — AttachmentMode

Also fills a stage-1 parity gap: the zod BasicUserActionsSchema was
missing ExtendedFieldsUserActionSchema and TemplateUserActionSchema,
so the zod UserActionPayload union was narrower than the io-ts one.
Both added to the union.

This PR is additive only. Type-check is clean and 112 schema parity
tests still pass. PRs 24b–d still required for the actual rename +
delete + tsconfig cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six zod-native files lived inside `common/types/{api,domain}/template/`
(the io-ts dirs). The zod mirrors at `common/types/{api_zod,domain_zod}/
template/v1.ts` did `export * from '../../{api,domain}/template/v1'` to
re-export them.

When PR 24d deletes the io-ts dirs, those re-exports become broken
self-loops after the rename. To prevent that, copy the zod-native
sources into the zod dirs so the content survives the deletion:

- domain_zod/template/v1.ts (replaces re-export)
- domain_zod/template/fields.ts
- domain_zod/template/translations.ts
- domain_zod/template/evaluate_conditions.ts
- domain_zod/template/validate_extended_fields.ts
- api_zod/template/v1.ts (replaces re-export; updates one import to
  use ../../domain_zod/template/v1 instead of ../../domain/template/v1)

The originals remain in place (consumers still resolve them through
the io-ts side) and will be deleted with the rest of the io-ts dirs in
PR 24d. 103 template-related tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…PR 24c)

Pre-rename consumer-side prep. Two changes:

1) Inline enums + replace io-ts cross-imports with local definitions
   in the four zod files that still cross the io-ts/zod boundary.
   After PR 24d's rename these would otherwise become self-loops:
   - domain_zod/custom_field/v1.ts — inlines CustomFieldTypes enum
   - domain_zod/attachment/v1.ts — inlines AttachmentType and
     ExternalReferenceStorageType enums
   - domain_zod/case/v1.ts — inlines CaseSeverity enum, drops two
     unused bundled-types imports
   - api_zod/user_action/v1.ts — switches UserActionTypes import to
     ../../domain_zod/user_action/action/v1 (already inlined in PR 24a)

2) Switch CaseStatusSchema and CaseSeveritySchema to z.nativeEnum so
   the inferred types are the TS enums (CaseStatuses, CaseSeverity)
   instead of literal unions. Previously the bundled schemas inferred
   as `'closed' | 'in-progress' | 'open'`, forcing ~80 cast errors at
   consumer call sites that operate on the enum types. nativeEnum
   collapses that gap in one place.

Pre-rename dry-run after these changes: 660 errors (initial attempt)
→ 250 (after PR 24a) → 117 (after enum inlining) → ~30 (after the
nativeEnum switch). Remaining errors are concrete shape mismatches
PR 24d will address case-by-case during the actual rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final stage of the io-ts → zod migration. After this commit the cases
plugin no longer references io-ts, fp-ts, or @kbn/securitysolution-io-ts-utils.

Mechanical changes:
- Deleted the io-ts directories (common/types/{api,domain}, common/schema)
  and helpers (common/api/{runtime_types,saved_object}.ts,
  server/common/runtime_types{,.test}.ts).
- Renamed `*_zod` directories and files to their canonical names:
    common/types/api_zod   → common/types/api
    common/types/domain_zod → common/types/domain
    common/schema_zod      → common/schema
    server/common/runtime_types_zod{,.test}.ts → runtime_types{,.test}.ts
- Bulk-rewrote `_zod` import paths across consumer files.
- Dropped `@kbn/securitysolution-io-ts-utils` from
  x-pack/platform/plugins/shared/cases/tsconfig.json kbn_references.
- Removed `throwErrors` (no longer exists) from common/index.ts and the
  io-ts re-exports from common/api/index.ts.

Schema parity fixes uncovered during the rename dry-run (the remaining
~30 errors PR 24c didn't address):
- domain/user/v1: User fields use `z.union([string, null, undefined])`
  (required keys with possibly-undefined values) so the inferred type
  matches the persisted-attributes `User` interface; avatar fields are
  optional to match the original io-ts `rt.partial`.
- domain/case/v1: add the `extended_fields_labels` record (was present
  in io-ts but missing from the bundled zod schema).
- domain/attachment/v1: re-export the `IsolateHostActionType` enum.
- domain/attachment/v2: `attachmentId` on UnifiedReferenceAttachment*
  schemas accepts `string | string[]` to match the original io-ts.
- domain/user_action/action/v1: cast UserActionActionsSchema enum
  values to the literal-union type (not `[string, ...string[]]`) so
  consumers see the proper `UserActionAction` literal.
- domain/user_action/status/v1: add optional `closeReason` and
  `syncedAlertCount` to the StatusUserAction payload.
- api/case/v1: add `extendedFieldFilters` to CasesSearchRequestSchema.
- api/user_action/v1: re-export `UserActionInternalFindResponse` and
  type the find request types as the proper literal union.
- api/attachment/v2: re-export `BulkGetAttachmentsRequestV2`.

Targeted consumer fixes:
- server/services/user_actions/transform.ts: cast
  `userAction.attributes.payload` (Record<string, unknown>) to
  `UserActionAttributes['payload']` at the function boundary.
- server/client/attachments/bulk_get.ts: cast the legacy-mode decode
  result via `unknown` to BulkGetAttachmentsResponseV2 (V1 ⊂ V2).

Validation: type_check on the cases project passes; eslint --fix on
the plugin clears all duplicate-import errors introduced by the bulk
rename; check_changes.ts is green; representative jest suites
(runtime_types, user_actions/transform) pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix three inline-snapshot mismatches surfaced by the full Jest run
after PR 24c/24d. The "and N more" counts in zod's error messages
went up because the schemas legitimately gained fields restored from
the io-ts source-of-truth:

- StatusUserActionPayloadSchema gained closeReason + syncedAlertCount
- CaseAttributesSchema gained extended_fields_labels
- UnifiedReferenceAttachmentPayloadSchema.attachmentId became
  string | string[]

Each test still asserts the same behavior (invalid input throws);
only the count of reported issues increased:

- services/attachments/index.test.ts:108  "and 25 more" → "and 26 more"
- services/attachments/index.test.ts:208  "and 25 more" → "and 26 more"
- services/user_actions/index.test.ts:1868 "and 24 more" → "and 28 more"

Snapshots updated by hand (no `jest -u`) so the diff stays auditable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.ts
#	x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts
#	x-pack/platform/plugins/shared/cases/server/client/cases/create.ts
Three behavioral regressions surfaced by porting the deleted
common/schema/index.test.ts to zod:

- NonEmptyString accepted whitespace-only strings ("   ", "\t\n");
  io-ts rejected them via s.trim() !== ''. Replace z.string().min(1)
  with a refinement on trimmed length so "   " is rejected and the
  original (untrimmed) value is preserved on success.

- paginationSchema silently coerced non-numeric page/perPage strings
  to NaN (z.string().transform((s) => Number(s))), bypassing the
  downstream > maxPerPage / MAX_DOCS_PER_PAGE guards. Move the parse
  onto the union and emit a "cannot parse to a number" issue when
  Number(s) is not finite, matching io-ts NumberFromString.

- limitedStringSchema rejected empty / whitespace-only strings even
  with min === 0; io-ts only rejected when min > 0. Gate the empty
  check on min > 0 so the "min: 0, max: N" cases accept "" / "  "
  as before.

Restore the deleted unit-test coverage: a zod-native port of
common/schema/index.test.ts (38 cases locking the three regressions
above and the rest of the helpers), and the four template
business-logic test files that were swept up in the codec cleanup
but cover pure functions independent of io-ts:

- evaluate_conditions.test.ts (compound condition evaluator)
- fields.test.ts (field schema helpers)
- validate_extended_fields.test.ts (extended-field validation)
- v1.test.ts (Template / CreateTemplateInput / PatchTemplateInput
  zod schemas)

Add three DeepStrict regression tests in runtime_types.test.ts
covering excess keys inside arrays of objects, union variants, and
discriminated-union variants — the cases most likely to drift from
io-ts exactCheck parity at depth.

Tighten the decodeOrThrowZod docstring to clarify that strip-on-
unknown matches rt.exact (rt.strict rejected; for that behavior use
decodeWithExcessOrThrowZod).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Many field-level jsdoc comments and section headers were dropped when
the io-ts schemas were rewritten as zod. They captured non-obvious
domain detail (e.g. that `closeReason` is the value synced to
attached alerts; that the `extended_fields` enum key intentionally
uses snake_case to match the SO attribute name returned by
`Object.keys(updatedAttributes)` in `getUserActionItemByDifference`)
that's not derivable from the schema shape alone.

Restored across 12 files (~100 jsdocs total):

- common/types/api/case/v1.ts (33 + section headers): all field
  descriptions on CaseBaseOptionalFieldsRequestSchema,
  CaseRequestFieldsSchema, CasePostRequestSchema,
  CasesFindRequestBaseFieldsSchema, CasesSearchRequestSchema and
  related Schemas; restored the "search cases" section header.
- common/types/api/metrics/v1.ts (24): every field on
  StatusInfo, AlertHostsMetrics, AlertUsersMetrics,
  SingleCaseMetrics, CasesMetricsRequest/Response.
- common/types/api/configure/v1.ts (10): connector / closure_type /
  owner / customFields / templates and CustomField / Template inner
  fields on the Request schema.
- common/types/api/attachment/v1.ts (2): the
  AttachmentPatchRequestSchema partial-update warning, and the
  sortOrder pagination field.
- common/types/api/stats/v1.ts (3): from / to / owner on
  CasesStatusRequestSchema.
- common/types/api/user_action/v1.ts (2 section headers): "User
  actions stats API" and "Find User Actions API".
- common/types/domain/case/v1.ts (12): every field on
  CaseBaseFields plus status / owner on CaseBasicSchema, and the
  enrichCasesWithFieldLabels comment on CASE_EXTENDED_FIELDS_LABELS.
- common/types/domain/configure/v1.ts (12): all CustomField /
  Template / Configuration field descriptions.
- common/types/domain/attachment/v1.ts (1): the
  AttachmentPatchAttributesSchema partial-update warning.
- common/types/domain/attachment/v2.ts (6): the Unified
  Reference / Value / Attributes / Schema / Partial /
  AttachmentMode block-level docs.
- common/types/domain/user_action/v1.ts (3): the
  CaseUserActionInjectedDeprecatedIdsSchema @deprecated pointer,
  UserActionAttributesSchema scope, and "User actions" section.
- common/types/domain/user_action/action/v1.ts (3): the
  UserActionTypes "do not remove" preamble, the
  extended_fields snake_case explanation, and the
  UserActionType / UserActionAction type docstrings.

Pure comment changes — type check passes (cases tsconfig).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lgestc and others added 3 commits May 8, 2026 10:34
Restores per-schema validation coverage for the most-used request
shapes — the gap flagged in the Cases reviewer pass on this PR.
Route-handler and cases_api_integration tests still cover the
end-to-end paths, but these unit suites lock the schema contract
itself so any future drift between an exported TS type and the
zod schema's inferred shape is caught at the schema layer instead
of on a route trip.

Adds:

- common/types/api/case/v1.test.ts (43 cases) covering
  CasePostRequestSchema (length / array / customField / connector
  excess-key behavior, unknown-field stripping, NonEmpty / trim
  parity), CasePatchRequestSchema (required id/version, partial-
  update guards, whitespace title rejection),
  CasesPatchRequestSchema (cases array bounds),
  CasesFindRequestSchema (page/perPage NumberFromString parity,
  perPage cap, array-vs-string filter forms, MAX_*_FILTER_LENGTH
  array bounds, sortField/searchField enum dispatch,
  decodeWithExcessOrThrowZod), and CasesSearchRequestSchema
  (searchFields enum, extendedFieldFilters shape).

- common/types/api/attachment/v1.test.ts (37 cases) covering
  AttachmentRequestSchema across all six variants (user, alert,
  event, actions, externalReference NoSO + SO, persistableState)
  with happy paths and per-variant required-field rejection,
  AttachmentPatchRequestSchema (the partial-update guard — patch
  body must carry the full type payload + id + version),
  BulkCreateAttachmentsRequestSchema bounds,
  BulkDeleteFileAttachmentsRequestSchema (NonEmptyString id parity
  including the whitespace case), and
  BulkGetAttachmentsRequestSchema bounds.

All 80 cases pass; type check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The schema unit tests added in 9905dfc imported
decodeWithExcessOrThrowZod from server/common/runtime_types — a
common→server import that violates the layering rule (common code
must not depend on server code).

Replace those references with a direct DeepStrict(schema) call from
@kbn/zod-helpers (already a declared dep), which is what
decodeWithExcessOrThrowZod wraps. The tests assert the same
behavior — a DeepStrict-wrapped schema rejects unknown top-level
fields — and now stay within common.

All 80 schema tests still pass; type check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lgestc lgestc marked this pull request as ready for review May 8, 2026 09:01
@lgestc lgestc requested a review from a team as a code owner May 8, 2026 09:01
@lgestc lgestc added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting Team:Cases Security Solution Cases team labels May 8, 2026
@infra-vault-gh-plugin-prod
Copy link
Copy Markdown

Pinging @elastic/kibana-cases (Team:Cases)

lgestc and others added 10 commits May 8, 2026 11:17
GetCaseUsersResponseRt was renamed to GetCaseUsersResponseSchema in the
io-ts → zod migration. The cases_api_integration test still imported
the old name, breaking the x-pack/platform/test type-check on CI
(missed in the original audit because the multi-line scan only covered
x-pack/solutions, x-pack/plugins, and src — not x-pack/platform/test).

The .encode() call is replaced with .parse() — both validate the shape
and throw on mismatch, which is what the surrounding try/catch expects.

Verified by running:
  node scripts/type_check --project x-pack/platform/test/tsconfig.json
Replaces the inline `any`-typed safeParse helper with a properly
typed `(schema: ZodType<unknown>, value: unknown) => string[]` and
narrows the `(result.data.connector as any).foo` cast to
`Record<string, unknown>`. Auto-fixes prettier wrapping reported by
check_changes (eslint --fix).

No behavioral changes — all 80 schema tests still pass; type-check
and lint clean.
The io-ts→zod migration aliased CommentUserActionWithoutIdsSchema to
CommentUserActionSchema, losing the distinction between the with-refs
and without-refs payloads. The persisted user-action payload has
externalReferenceId (and persistable-state SO refs) extracted into
the SO's references array, so validating against the with-refs schema
rejected file/persistable-state comment user actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The get_case_metrics integration test asserted the io-ts error message
"Invalid value \"bananas\" supplied to \"features\"". After the zod
migration, decodeWithExcessOrThrowZod runs the request through
stringifyZodError, which formats union-literal failures as
'Invalid input: expected "<literal>"' per branch. Update the assertion
to match the new format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Resolve merge conflicts in 8 files (comment/file/lens attachments, server
  services, client utils, internal attachments) — keep zod functions but
  adopt structural changes from main (new inject steps, simplified paths)
- Fix domain_zod/attachment/file/v2.ts: update import of
  SingleFileAttachmentMetadataSchema from non-existent ../v1 to the canonical
  ../../../domain/attachment/v1 path (our branch moved domain_zod → domain)
- Fix FTR test assertion in get_case_metrics.ts: the zod union-literal error
  for 6 branches hits MAX_ERRORS=5 so "lifespan" (6th) becomes "and 1 more";
  update assertion to check "features.0: Invalid input: expected "alerts.count""
  which reliably appears first

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- transform.ts: add missing UserActionAttributes['payload'] cast to the
  early-return path in addReferenceIdToPayload (matches the existing cast
  at the bottom of the function)
- transform.test.ts: route the payload override through unknown to avoid
  an unsafe direct cast that TypeScript now rejects
- services/attachments/index.ts: replace stale io-ts names decodeOrThrow /
  AttachmentAttributesRtV2 with zod equivalents decodeOrThrowZod /
  AttachmentAttributesSchemaV2 in the unified find path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Zod v4's stringifyZodError flattens invalid_union inner errors which
carry empty paths (not the parent array index path). The assertion was
checking for the io-ts-style path prefix 'features.0:' which is absent
in the actual zod error output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restores 37 deleted test files covering api/ and domain/ schema types,
rewritten to verify the Zod schemas (safeParse) rather than the removed
io-ts codecs. All previously covered schemas are tested including full
attachment types, CasesSchema, and nested-stripping behaviour for
metrics, configure, connector, and case attribute schemas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kibanamachine
Copy link
Copy Markdown
Contributor

kibanamachine commented May 15, 2026

💔 Build Failed

Failed CI Steps

Metrics [docs]

✅ unchanged

History

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

Labels

backport:skip This PR does not require backporting release_note:skip Skip the PR/issue when compiling release notes Team:Cases Security Solution Cases team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants