-
Notifications
You must be signed in to change notification settings - Fork 37
Schema Caching For Request and Response Bodies #187
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
Schema Caching For Request and Response Bodies #187
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #187 +/- ##
==========================================
+ Coverage 97.09% 97.21% +0.12%
==========================================
Files 41 42 +1
Lines 4403 4597 +194
==========================================
+ Hits 4275 4469 +194
Misses 128 128
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
I just want to say:
This is fantastic. Thank you! |
4c50f83 to
8076910
Compare
| description: This number starts its journey where most numbers are too scared to begin! | ||
| exclusiveMinimum: true | ||
| minimum: 10`, | ||
| version: 3.0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what was happening before, but exclusiveMinimum being true is an OpenAPI 3.0 feature and isn't valid in 3.1 (which I imagine is why you have these unit tests). Just updated it to pass in the correct version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@k1LoW added a new version argument to set the correct version, this must be an artifact of that.
cache/cache.go
Outdated
| @@ -0,0 +1,26 @@ | |||
| // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2025 my good sir.
daveshanley
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the dates and this looks good.
cache/cache_test.go
Outdated
| @@ -0,0 +1,308 @@ | |||
| // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2025
| description: This number starts its journey where most numbers are too scared to begin! | ||
| exclusiveMinimum: true | ||
| minimum: 10`, | ||
| version: 3.0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@k1LoW added a new version argument to set the correct version, this must be an artifact of that.
Oct 14th 2025 Update
TL;DR -
SchemaCacheinterface. If one isn't provided then we will use the local sync.Map by default. This allows a developer to pass innilif they want to disable caching all together or they can pass in their own implementation that matches the interface if they have a better alternativeValidate[Request|Response]Schemafunctions so they take in a struct rather than a series of arguments. This will allow us to add more in the future if needed without being a breaking change.Validate[Request|Response]Schemafunctions so that they no longer take in therenderedSchemaandjsonSchemaarguments. Instead, a user passes in the*base.Schemathey got from libopenapi and the function now handles rendering those objects for them.With the latest commit, I merged the version changes that were pushed to main as well as swapped over to the struct input we discussed earlier. Additionally, I've tried to simplify the client interface by removing the
jsonSchemaandrenderedSchemaarguments that were previously present on theValidate[Request|Response]Schemafunctions. Instead, the client just needs to pass in their*base.Schemaobject and the function will handle generating those for them. As a result, the logic that previously did this in thevalidate_body.gofiles has been removed as well.The caveat of this approach is that some of your unit tests were passing in manually crafted
*base.Schemaobjects. These didn't have thelowfield populated which meant we couldn't render the schema from them. In order to get the unit tests working, I had to generate the schemas from the libopenapi package which requires taking in a complete OpenAPI spec. In my opinion, this is a more accurate experience as developers using libopenapi-validator should be usinglibopenapias you pass the doc to create the validator, but I wanted to callout that this was now a requirement.Overall though, the input struct now accepts:
*base.Schemato validate againstIf you'd prefer I "revert" the changes and leave it so the function still takes in the rendered schemas, I can do that. Just thought this might be a nice refactor to simplify the interface if someone is using this function directly. It also makes it so we only need to interact with the cache in one spot versus two like my PR had before.
Warning: This is a breaking change
Updated: Refactored
ValidateRequestSchemaandValidateResponseSchemato use a struct-based API to prevent future breaking changes. The functions now take a single struct parameter with functional options support.TL;DR
What: Pre-compile and cache JSONSchemas instead of recompiling on every request.
Why: The library was creating ~100KB of garbage per request by recompiling schemas, causing high memory pressure and frequent GC pauses.
Impact: 6-9x faster validation, 90% less memory, 11x fewer GC cycles.
Breaking Change:
ValidateRequestSchema/ValidateResponseSchemanow use struct-based API (future-proof for new parameters).Trade-off: Validator creation 2x slower (8ms vs 4ms), paid back after 2 requests.
Note: High-level
Validatorinterface unchanged - only affects direct function callers.Add compiled schema caching for 6-9x faster validation
Problem
Every request was compiling JSONSchemas from scratch, creating ~96KB of garbage per request and triggering GC every 22 requests. This caused high memory pressure, frequent GC pauses, and unpredictable tail latencies.
Solution
Pre-compile all JSONSchemas during validator initialization and reuse them across requests. The cache is eagerly warmed by walking the entire OpenAPI document.
Performance Impact
At 1000 req/sec: 102 MB/sec → 8.3 MB/sec allocated (10x reduction!)
Trade-offs
Pros:
Cons:
Implementation
Cache Structure
Added
helpers.SchemaCachestoring rendered YAML, JSON, and compiled schema.Cache Warming
On initialization, walks all paths → operations → request/response bodies → parameters, pre-compiling each schema.
Runtime
Breaking Changes
Why: Prevents future breaking changes when adding new parameters. Aligns with Go best practices for extensible APIs.
Migration:
renderedSchema,jsonSchema,compiledSchemaargs)[]config.Option{}or useconfig.WithExistingOpts(opts)to convert existing*ValidationOptionsNote: Most users use the
Validatorinterface (ValidateHttpRequest,ValidateHttpResponse), which are unchanged and fully backward compatible.Testing
When to Use
Ideal for:
Less ideal for:
For typical production use cases, this is a massive win.
Key Changes
API Refactor:
requests/validate_request.go- Struct-based API with internal schema rendering/compilationresponses/validate_response.go- Struct-based API with internal schema rendering/compilationrequests/validate_body.go- Updated to use new struct APIresponses/validate_body.go- Updated to use new struct APICaching:
validator.go- Eager cache warming (usesGetOperations()for completeness)helpers/schema_compiler.go-SchemaCachetype & sharedSchemaCacheInterfaceconfig/config.go- Default cache initialization viaNewValidationOptions()Tests: