-
Notifications
You must be signed in to change notification settings - Fork 38
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.
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 -
SchemaCache
interface. If one isn't provided then we will use the local sync.Map by default. This allows a developer to pass innil
if 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]Schema
functions 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]Schema
functions so that they no longer take in therenderedSchema
andjsonSchema
arguments. Instead, a user passes in the*base.Schema
they 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
jsonSchema
andrenderedSchema
arguments that were previously present on theValidate[Request|Response]Schema
functions. Instead, the client just needs to pass in their*base.Schema
object and the function will handle generating those for them. As a result, the logic that previously did this in thevalidate_body.go
files has been removed as well.The caveat of this approach is that some of your unit tests were passing in manually crafted
*base.Schema
objects. These didn't have thelow
field 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 usinglibopenapi
as 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.Schema
to 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
ValidateRequestSchema
andValidateResponseSchema
to 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
/ValidateResponseSchema
now 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
Validator
interface 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.SchemaCache
storing 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
,compiledSchema
args)[]config.Option{}
or useconfig.WithExistingOpts(opts)
to convert existing*ValidationOptions
Note: Most users use the
Validator
interface (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
-SchemaCache
type & sharedSchemaCacheInterface
config/config.go
- Default cache initialization viaNewValidationOptions()
Tests: