Hey all, general AWS lambda dev question here. Wha...
# help
r
Hey all, general AWS lambda dev question here. What are you all using to validate your JSON request bodies? I was leaning towards ajv but all the perf tests are assuming you can compile the validator once and then run validation over and over. With the lambda you will need to compile the validator every time it is cold started. Can't seem to find any numbers on this, but the @middy/validator seems to use ajv behind the scenes and mentions
Copy code
Important Compiling schemas on the fly will cause a 50-100ms performance hit during cold start for simple JSON Schemas. Precompiling is highly recommended.
I really would rather not get into a build step to pre-compile json schemas but also not interested in taking a 50-100ms performance hit. My latest experiment is using
io-ts
which I cant find many benchmarks on, just this one, https://github.com/gcanti/io-ts-benchmarks. I updated these with 2 new tests(
io-ts-init
and
ajv-setup
. I just exported a function instead of the cached type. So each test has to call the function first to get the validator. I also tested this with just in-lining the
io-ts
code right into the test (no calling a function) and the results are almost identical. So no major overhead in the function call. As you can see AJV sucks when you need to call setup over and over, though
io-ts
loses a lot also like 5x slower 😕 . Anyway curious what others have tried, failed, succeeded?
g
I normally use runtypes or zod
r
Thanks! hadn't seen zod, added it to the mix. Just using function call versions though at this point as that's what really seems to be the test. What can be called over and over from scratch and has the best performance.
Updated all the tests to run in a more "lambda like" way, each returns a function that recreates the whole validator.
ajv
way behind for this situation, definitely optimized for create once situations. Looking like
io-ts
with
is
is the best bet right now. Can check that and if its not valid can call decode to get an error, which is hopefully less likely. Though most of these are so far sub MS its probably fair to go either way. forked repo here. https://github.com/aphex/io-ts-benchmarks I am going the path of adding this to my
body-parser
util with
io-ts
for now.
Latest results
a
Thanks for the info. Are they all using a JSON Schema declaration as input, or do the faster ones require duplicating your schema definition in code?
t
Wow didn't know ajv was so slow
also surprised at how much better io-ts is over zod
r
@Adam Fanello good question, I cant speak to how other libraries work with this but I assume you are working in a TS world? Maybe you have a use case for describing your schema as a standard JS object though? I would think if you are working in JS just describing your schema in whatever syntax wouldn't matter to much. However for TS this is a problem. You do not want to make a type definition and a schema if you can avoid it. Ideally you would write one and generate the other. That is exactly what we are doing with io-ts and why we picked it in the first place. It can generate typedefs from a schema like this, in reference to this benchmark (https://github.com/aphex/io-ts-benchmarks/blob/master/src/space-object/io-ts.ts)
export type IOTSType = t.TypeOf<ReturnType<typeof getType>>
So now IOTSType will just be the type for an object following your schema. This also plays very nicely with io-ts's
is
method as it works as a type guard. if
(<http://MyType.is|MyType.is>(body))
then body is of type
MyType
. To make this easier we name our schema and our types the same.
Copy code
const MyType = t.type({...}
and
Copy code
export type MyType = t.TypeOf<typeof MyType>
Not totally sold on whether this makes things more or less confusing yet 🙂 but with
import
and
import type
we can get to these even if we need to. However generally we done export the schema, just the type. From what I can tell the Type info is totally stripped away by esbuild so this has no effect on performance.
a
There's a third schema: OpenAPI/JSON schema. I'm trying to not define types three times. While starting with with validation schema is possibly the most descriptive, I find it hardest to read. I like a nice simple Typescript
interface
definition of my types, with JsDoc describing things as needed. I think what I want is to hand write OpenAPI, and have it generate Typescript interfaces and validation functions.
r
@Adam Fanello dude I am with you on this one. I went down that road a few months back and I finally landed on io-ts with openapi jsdocs. Did you happen to find anything better? I know i have to type things twice, but I am less bothered as I consider one documentation and the other implementation.
a
With teams of developers, I can't trust that the two sources will remain in sync. I've researched, but haven't tried implementing anything yet. Here is my candidate toolchain to try: ---- • Write API models in OpenAPI ◦ Use directly for documentation ◦ Single source of truth, DRY • Convert OpenAPI to JSON Schema • Convert JSON Schema to Typescript model • Generate static Typescript for validating input per model ◦ Pro: no reflection and no repetition ◦ Con: loses constraints from original OpenAPI? Other options: https://itnext.io/parse-dont-validate-incoming-data-in-typescript-d6d5bfb092c8
r
The real problem is these concerns live in very different places. openapi being documentation and TS/JS being in code. We could possibly create a parser for io-ts though, something that scan code for io-ts types and generates openapi types. However thats likely not enough as handler endpoints need more then just body structure. Also this puts us into a position where we would be running a openapi docs pass, some build script to parse the code. I fear its more trouble then its worth.
Very interesting, and fair to assume that the conversion from openapi to JSON schema would not happen all that often. Hopefully you're not changing it to much
I am on the other side of this, I am trusting developers to be good 🙂 good documentation is just as important as good code. Plus the openapi generated content will be dogfooded by other devs on the team. So there is a shame aspect to those who dont keep up 😛 Plus PR reviews
For the record the process you have there is I think the best it gets right now using existing tools, if you do want a single source to start with. You need to start with openapi and generate JSON, TS, and validation. However it may be possible to generate
io-ts
code from your OpenAPI, this could be a very welcome pacakge for the community that is interested in running spec change scripts. So you would write the openapi and generate once, it would create a io-ts schema, TS types and inherently a validator for each type. Again this is a step you need to run anytime you change the spec, but I think that's what you're going for.
Welp nevermind, there are no original ideas left 🙂 https://github.com/Fredx87/openapi-io-ts and https://github.com/pagopa/openapi-codegen-ts
Knowing this exists makes me question my stance now haha, ONE build step isnt that bad... gonna have to rethink this 😛 might even be able to make a watcher for the openapi spec that rebuilds these on demand, and add a pre-install hook to run them anytime a new dev pull down the code. Then keep them out of version control... hmmmmm
a
Yeah. I always include an "analyze" NPM script in my projects. It runs prettier, unit tests, and eslint. That has to pass before making a PR. Can add the generator script to this so that it doesn't get forgotten.
r
heck ya
a
Oh but yes, could have it as a pre-build script and not commit.
@Ross Gerbasi Neither of those two you found support OpenAPI v3.0. 😞 (One depends on a tool that hasn't changed in two years, and the other depends on a parser that does support OpenAPI v3, but is using a 3-year old version that doesn't.) I'm looking at the https://swagger.io/tools/swagger-codegen/. It's active and supports OpenAPI v3 and can generate lots of stuff. None of the generators do exactly what I want but is an extensible platform on which I can build what I want. 🤞
r
Really i am messing openapi-io-ts right now it seems to be OAS3?
a
Dependency: "swagger-parser": "^7.0.0", Current version is 10; 7.x is 3 years old which predates OAS3. (I also want explicit Typescript interfaces with schema and property descriptions translated to JsDoc, not t.TypeOf<> magic.)
r
Thats what i got
given this
Copy code
openapi: 3.0.0
info:
  title: My Schema
  version: 1.0.0
paths:
  /test/thing:
    post:
      operationId: test
      description: test
      requestBody:
        required: true
        content: 
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: string
                name: 
                  type: string
              required: 
                - id
                - name
      responses:
        '200':
          description: Same Stuff.
          content:
             application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name: 
                    type: string
components: {}
tags: []
It generated this
Copy code
import { ApiError, ApiResponse, HttpRequestAdapter, Operation, request } from "@openapi-io-ts/runtime";
import { TaskEither } from "fp-ts/TaskEither";
import * as t from "io-ts";



export const TestRequestBodySchema = t.type({
    id: t.string,
    name: t.string
})

export interface TestRequestBodySchema {
    id: string,
    name: string
}

export const TestResponse200Schema = t.partial({
    id: t.string,
    name: t.string
})

export interface TestResponse200Schema {
    id?: string,
    name?: string
}

export const testOperation: Operation = {
    path: "/test/thing",
    method: "post",
    responses: { "200": { _tag: "JsonResponse", decoder: TestResponse200Schema } },
    parameters: [],
    requestDefaultHeaders: { "Content-Type": "application/json", "Accept": "application/json" },
    body: {
        _tag: "JsonBody"
    }
}

export const testBuilder = (requestAdapter: HttpRequestAdapter) => (body: TestRequestBodySchema): TaskEither<ApiError, ApiResponse<TestResponse200Schema>> =>
    request(testOperation, {}, body, requestAdapter);
a
I see it made some interfaces - that's promising. If you put "description" on the properties, do those output?
r
description seems to be stripped, but I am not sure where you are hoping it would end up? a comment in the TS declaration?
a
As /** JsDoc */
r
I would think description would end up in the generated docs from the schema?
ah i see, ok well thats not totally needed haha but yeah i could see why that could be a nice to have
a
When coding though, I want to see it there.
If that's the only thing missing, I could probably contribute an enhancement to the project to add it.
r
yeah and maybe update the parser?
probably doesnt seem that hard to add a comment to attributes also.
seems worth looking into to modernize this one. the swagger codegen was a whoel thing.
a
Something else I'm thinking about: https://www.lukeautry.com/blog/tserial/ This looks like the lightest/fastest solution. It's TS to validation. Would use something else for Schema to TS. Upside is really simple and fast. Downsides: no updates in two years and only validates from TS not things like numeric ranges that OpenAPI can specify.
That led me to a similar tool ts-auto-guard that just released v1.0 four days ago.
I realized that OpenAPI v3.0 has been around for a while; it's v3.1 that I was hoping for because it's about 99% superset of JSON Schema, unlike v3.0. None of these tools say they support v3.1 yet. From openapi-io-ts:
Note: At the moment the only OpenAPI version supported is 
3.0
. If you have a 
2.0
 file you have to convert it to 
3.0
 before. Version 
3.1
 is not supported at the moment.