Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HTTP/OAS] zod support #186190

Merged
merged 43 commits into from
Jul 19, 2024
Merged

[HTTP/OAS] zod support #186190

merged 43 commits into from
Jul 19, 2024

Conversation

jloleysens
Copy link
Contributor

@jloleysens jloleysens commented Jun 13, 2024

Summary

Enable Core's router to directly accept @kbn/zod runtime validation schemas for validation and generate OpenAPI specification (OAS) from them.

Rationale

We have enabled a code first approach to OAS based on runtime validation (see epic) and would like to ensure that this platform capability is available to the maximum number of Kibana developers. Kibana developers use runtime validation libs other than @kbn/config-schema for a variety of reasons. Generally they fall under these themes:

  • Performance: there have been cases were Joi was not performant enough for validating certain schemas
  • Team preferences: some teams prefer the language, API or feature set offered by another runtime validation library (see for ex. use of io-ts's is function). Even if, arguably, there are no strong technical reasons for this approach teams have built certain practices and expectations around them.
  • Validation on the front end: notably validating large JSON blobs of user content. @kbn/config-schema is not suitable for this due to it's raw size (not a problem server side of course).

zod stands out among runtime validation libraries in Kibana (big 3 are @kbn/config-schema, zod and io-ts). zod offers a pragmatic API and the ability to derive JSONSchema for generating OAS. zod is fairly widely adopted in the broader JS ecosystem and gets regular fixes and updates. For these reasons zod is a good fit for Kibana's API needs and likely the library we'd choose today in some parallel universe.

Contributions welcome

This PR was productionized and driven forward largely by @maryam-saeidi 's efforts and investigation into o11y team's adoption of @kbn/config-schema and a subsequent investigation (based on this PR) into zod. If teams have specific needs from either @kbn/config-schema or zod please consider making an upstream contribution before picking yet another runtime validation lib!!

What about @kbn/config-schema?

The general advice for (new) developers is to continue using @kbn/config-schema.

Core's position is that we will continue to recommend @kbn/config-schema as the primary, first-class-citizen runtime validation library for all our purposes (SO schema, configuration, API schema) and consider it in long-term maintenance mode. At the same time, we recognise that zod is viable alternative for HTTP APIs that can help increase (o11y) team's investment in Kibana platform-provided capabilities like OAS generation from code.

Usage

Here is an example of using Zod in SLO delete API.

Risks

  • No present risks of breaking any behaviour as no API is passing @kbn/zod to Kibana router declarations
  • Another 3rd party library:
    • Longer term, lower risk possibility of the library becoming unmaintained. Mitigation: given the growing number of projects depend on zod this seems unlikely and it is always possible for us to contribute to zod directly. The trade-off with value unlocked for Kibana developers seems worth it in this case. Additionally, we wrap zod as @kbn/zod which enables us to shim/fix/improve a large number of aspects of zod like any missing OAS features.
    • Removes Kibana Core as the bottleneck for teams if they are looking for specific, modern features zod supports or will support in future (e.g. transform API).

Notes

  • Replacing joi with zod is a nice goal, but has proven practically infeasible due to their differences in API and behaviour: unfortunately @kbn/config-schema is a leaky wrapper around joi.

Follow up work

Copy link
Contributor Author

@jloleysens jloleysens left a comment

Choose a reason for hiding this comment

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

I added a couple of commits (mostly deleting code), otherwise this is looking in pretty good shape!

We'll have to get someone else from Core to do a final final review. But let's make sure CI is happy first :)

@jloleysens
Copy link
Contributor Author

/ci

@maryam-saeidi
Copy link
Member

/ci

@jloleysens
Copy link
Contributor Author

/ci

@kibana-ci
Copy link
Collaborator

💛 Build succeeded, but was flaky

Failed CI Steps

Metrics [docs]

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/core-http-server 193 195 +2
Unknown metric groups

API count

id before after diff
@kbn/core-http-server 495 497 +2

ESLint disabled line counts

id before after diff
@kbn/router-to-openapispec 1 2 +1

Total ESLint disabled count

id before after diff
@kbn/router-to-openapispec 1 2 +1

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@jloleysens jloleysens marked this pull request as draft July 11, 2024 12:20
Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

Approach looks good to me, no major concerns in the PR's current state

@jloleysens jloleysens marked this pull request as ready for review July 12, 2024 08:16
Copy link
Member

@maryam-saeidi maryam-saeidi left a comment

Choose a reason for hiding this comment

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

After this PR is merged, I will update our observability API guidelines to use Zod 🎉

@jloleysens jloleysens changed the title [OAS] zod support [HTTP/OAS] zod support Jul 17, 2024
Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

LGTM


import { schema } from '@kbn/config-schema';
import type { ZodType } from '@kbn/zod';
import { schema, Type } from '@kbn/config-schema';
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT Type can be imported as a type

@@ -119,6 +120,8 @@ export class RouteValidator<P = {}, Q = {}, B = {}> {
): T {
if (isConfigSchema(validationRule)) {
return validationRule.validate(data, {}, namespace);
} else if (isZod(validationRule)) {
return validationRule.parse(data);
Copy link
Contributor

Choose a reason for hiding this comment

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

Namespace is not passed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is part of why Zod is not a drop-in replacement for Joi, no passing context down via the parse method.

import type { OpenAPIV3 } from 'openapi-types';
// eslint-disable-next-line import/no-extraneous-dependencies
import zodToJsonSchema from 'zod-to-json-schema';
import { KnownParameters } from '../../type';
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT can be imported as a type

return new Error(`[Zod converter] ${message}`);
};

function assertInstanceOfZodType(schema: unknown): asserts schema is z.ZodTypeAny {
Copy link
Contributor

Choose a reason for hiding this comment

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

asserts? TIL ❤️

if (instanceofZodTypeKind(type, z.ZodFirstPartyTypeKind.ZodLazy)) {
return unwrapZodType(type._def.getter(), unwrapPreprocess);
}
if (instanceofZodTypeKind(type, z.ZodFirstPartyTypeKind.ZodEffects)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same processing for the 3 scenarios? Seems odd.
Are there any other options that don't require unwrapZodType?
Perhaps it'd be interesting to highlight / illustrate them somehow.

if (zodSupportsCoerce) {
if (!instanceofZodTypeCoercible(subShape)) {
throw createError(
`Input parser key: "${shapeKey}" must be ZodString, ZodNumber, ZodBoolean, ZodBigInt or ZodDate`
Copy link
Contributor

@gsoldevila gsoldevila Jul 19, 2024

Choose a reason for hiding this comment

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

Neither in instanceofZodTypeCoercible nor ZodTypeCoercible (above) you don't have a condition for ZodString. Is that normal?

@gsoldevila
Copy link
Contributor

gsoldevila commented Jul 19, 2024

There's quite a lot of logic for zod's OAS converter.
Isn't there any existing library that performs this adaptation?
Haven't checked in detail, but perhaps worth exploring.

@jloleysens
Copy link
Contributor Author

jloleysens commented Jul 19, 2024

Isn't there any existing library that performs this adaptation?

Good catch @gsoldevila ! Yes there is, this code was copy-pasta'd largely from there. Kinda being treated as a "black box" that we can open if/when we need to.

Because we want to do a few custom things with our OAS, we just cherry-picked the parts that convert the runtime schema to JSONSchema.

Copy link
Contributor

@gsoldevila gsoldevila left a comment

Choose a reason for hiding this comment

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

Pre-approving as most of my remarks are related to copy-pasted code

@maryam-saeidi
Copy link
Member

@gsoldevila Thanks for reviewing this PR!

Regarding the OAS convertor, we will need to revisit it when we use Zod in our products. During my investigation, I noticed we are missing some features, and we need to verify the convertor's result in an actual scenario.

So, I will merge this PR, and we can check your comments in a follow-up PR.

@maryam-saeidi maryam-saeidi enabled auto-merge (squash) July 19, 2024 14:25
@jloleysens jloleysens enabled auto-merge (squash) July 19, 2024 15:17
@jloleysens jloleysens merged commit 6a7a400 into main Jul 19, 2024
36 checks passed
@jloleysens jloleysens deleted the oas/zod-poc branch July 19, 2024 15:53
@kibanamachine kibanamachine added v8.16.0 backport:skip This commit does not require backporting labels Jul 19, 2024
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

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 - 4 +4
Unknown metric groups

API count

id before after diff
@kbn/zod - 1224 +1224

ESLint disabled line counts

id before after diff
@kbn/router-to-openapispec 1 2 +1

Total ESLint disabled count

id before after diff
@kbn/router-to-openapispec 1 2 +1

Unreferenced deprecated APIs

id before after diff
@kbn/zod - 4 +4

History

cc @jloleysens @maryam-saeidi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting Feature:http Feature:OAS Work or issues related to Core-provided mechanisms for generating OAS release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Team:Observability Team label for Observability Team (for things that are handled across all of observability) v8.16.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants