From 565061979d2a3ad1fa1e3ab8ba7b282b80bc3bb9 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 29 Apr 2026 15:29:02 +0800 Subject: [PATCH] feat: add skip_validation custom hook for value-level bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new `skip_validation` option to `generate_validator()`. When provided, the generated validator emits a check at the start of each sub-validator: if `custom.skip_validation(value)` returns true, the value is accepted without further constraint checks (type, enum, format, pattern, range, etc.). This is useful for placeholder strings (e.g. secret references like `$secret://vault/key`) that will be resolved at runtime — they need to pass schema validation even when the schema expects a non-string type. The hook is opt-in: when not provided, no extra code is generated and there is zero performance overhead. --- lib/jsonschema.lua | 17 ++++- t/default.lua | 151 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 1 deletion(-) diff --git a/lib/jsonschema.lua b/lib/jsonschema.lua index f8871ca..c0d08da 100644 --- a/lib/jsonschema.lua +++ b/lib/jsonschema.lua @@ -335,6 +335,7 @@ local function codectx(schema, options) -- schema management _validators = {}, -- maps paths to local variable validators _external_resolver = options.external_resolver, + _skip_validation = options.skip_validation, }, codectx_mt) self._root = self return self @@ -618,6 +619,19 @@ generate_validator = function(ctx, schema) return ctx end + -- skip_validation hook: if the caller provided a predicate via + -- custom.skip_validation, check it before any constraint. When it + -- returns true the value is accepted as-is (useful for placeholder + -- strings like secret references that will be resolved at runtime). + -- The predicate receives (value, schema) so it can inspect the + -- expected type of the field being validated. + if ctx._root._skip_validation then + local schema_ref = ctx:uservalue(schema) + ctx:stmt(sformat('if %s ~= nil and %s(%s, %s) then return true end', + ctx:param(1), + ctx:libfunc('custom.skip_validation'), ctx:param(1), schema_ref)) + end + -- type check local tt = type(schema.type) if tt == 'string' then @@ -1206,7 +1220,8 @@ return { null = custom and custom.null or default_null, match_pattern = custom and custom.match_pattern or match_pattern, parse_ipv4 = custom and custom.parse_ipv4 or parse_ipv4, - parse_ipv6 = custom and custom.parse_ipv6 or parse_ipv6 + parse_ipv6 = custom and custom.parse_ipv6 or parse_ipv6, + skip_validation = custom and custom.skip_validation or nil, } local name = custom and custom.name local has_original_id diff --git a/t/default.lua b/t/default.lua index e4d36ad..42414e8 100644 --- a/t/default.lua +++ b/t/default.lua @@ -250,3 +250,154 @@ local pcall_ok, valid, val_err = pcall(validator, { "a", "b", "c" }) assert(pcall_ok, "fail: validator threw an error: " .. tostring(valid)) assert(valid, "fail: validator returned false: " .. tostring(val_err)) ngx.say("passed: recursive datatype is not shared across calls") + +----------------------------------------------------- test case 10 +-- skip_validation: type bypass - string value passes integer schema +local skip_fn = function(val, schema) + return type(val) == "string" and val:sub(1, 10) == "$secret://" +end + +rule = { + type = "object", + properties = { + host = { type = "string" }, + port = { type = "integer" }, + enabled = { type = "boolean" }, + }, + required = { "host", "port" }, +} + +validator = jsonschema.generate_validator(rule, { skip_validation = skip_fn }) +ok, err = validator({ host = "$secret://vault/host", port = "$secret://vault/port", enabled = "$secret://vault/flag" }) +assert(ok, "fail: skip_validation should bypass type checks: " .. tostring(err)) +ngx.say("passed: skip_validation bypasses type checks") + +----------------------------------------------------- test case 11 +-- skip_validation: enum bypass +rule = { + type = "object", + properties = { + scheme = { type = "string", enum = { "http", "https" } }, + }, +} +validator = jsonschema.generate_validator(rule, { skip_validation = skip_fn }) +ok, err = validator({ scheme = "$secret://vault/scheme" }) +assert(ok, "fail: skip_validation should bypass enum: " .. tostring(err)) +-- non-secret string should still be validated +ok, err = validator({ scheme = "ftp" }) +assert(not ok, "fail: non-secret value should still fail enum") +ngx.say("passed: skip_validation bypasses enum but normal values validated") + +----------------------------------------------------- test case 12 +-- skip_validation: default values still set for nil fields +rule = { + type = "object", + properties = { + host = { type = "string" }, + port = { type = "integer", default = 6379 }, + }, +} +validator = jsonschema.generate_validator(rule, { skip_validation = skip_fn }) +local conf = { host = "$secret://vault/host" } +ok, err = validator(conf) +assert(ok, "fail: skip_validation with defaults: " .. tostring(err)) +assert(conf.port == 6379, "fail: default value should still be set") +ngx.say("passed: skip_validation preserves default values") + +----------------------------------------------------- test case 13 +-- skip_validation: nested object properties still validated +rule = { + type = "object", + properties = { + upstream = { + type = "object", + properties = { + host = { type = "string" }, + port = { type = "integer" }, + }, + required = { "host" }, + }, + }, +} +validator = jsonschema.generate_validator(rule, { skip_validation = skip_fn }) +-- nested secret ref should pass +ok, err = validator({ upstream = { host = "$secret://vault/host", port = "$secret://vault/port" } }) +assert(ok, "fail: nested skip_validation: " .. tostring(err)) +-- whole object as secret ref should pass +ok, err = validator({ upstream = "$secret://vault/upstream" }) +assert(ok, "fail: object-level skip_validation: " .. tostring(err)) +ngx.say("passed: skip_validation works for nested objects") + +----------------------------------------------------- test case 14 +-- skip_validation: array items bypass +rule = { + type = "object", + properties = { + tags = { + type = "array", + items = { type = "string", minLength = 3 }, + }, + }, +} +validator = jsonschema.generate_validator(rule, { skip_validation = skip_fn }) +ok, err = validator({ tags = { "abc", "$secret://vault/tag" } }) +assert(ok, "fail: array items skip_validation: " .. tostring(err)) +ngx.say("passed: skip_validation works for array items") + +----------------------------------------------------- test case 15 +-- skip_validation: not configured - normal validation applies +rule = { + type = "object", + properties = { + port = { type = "integer" }, + }, +} +validator = jsonschema.generate_validator(rule) +ok, err = validator({ port = "$secret://vault/port" }) +assert(not ok, "fail: without skip_validation, string should fail integer type check") +ngx.say("passed: without skip_validation, normal validation applies") + +----------------------------------------------------- test case 16 +-- skip_validation: schema-aware hook - only bypass for string-typed fields +-- This demonstrates using the schema parameter to make smart decisions: +-- secret refs in string fields bypass constraints (enum, pattern, format), +-- but secret refs in non-string fields (integer/boolean) are rejected. +local schema_aware_skip = function(val, schema) + if type(val) ~= "string" or val:sub(1, 10) ~= "$secret://" then + return false + end + -- Only bypass when the schema expects a string type. + -- For non-string fields, do not skip — we don't allow string + -- placeholders in integer/boolean/object fields at validation time. + if not schema or schema.type ~= "string" then + return false + end + return true +end + +rule = { + type = "object", + properties = { + host = { type = "string", pattern = "^[a-z]+%.com$" }, + scheme = { type = "string", enum = { "http", "https" } }, + port = { type = "integer", minimum = 1, maximum = 65535 }, + enabled = { type = "boolean" }, + }, +} +validator = jsonschema.generate_validator(rule, { skip_validation = schema_aware_skip }) +-- secret ref in string+pattern field: bypassed (schema.type == "string") +ok, err = validator({ host = "$secret://vault/host" }) +assert(ok, "fail: schema-aware skip should bypass string+pattern: " .. tostring(err)) +-- secret ref in string+enum field: bypassed (schema.type == "string") +ok, err = validator({ scheme = "$secret://vault/scheme" }) +assert(ok, "fail: schema-aware skip should bypass string+enum: " .. tostring(err)) +-- secret ref in integer field: NOT bypassed (schema.type == "integer") +ok, err = validator({ port = "$secret://vault/port" }) +assert(not ok, "fail: schema-aware skip should NOT bypass integer field") +-- secret ref in boolean field: NOT bypassed (schema.type == "boolean") +ok, err = validator({ enabled = "$secret://vault/flag" }) +assert(not ok, "fail: schema-aware skip should NOT bypass boolean field") +-- non-secret invalid enum value: still fails +ok, err = validator({ scheme = "ftp" }) +assert(not ok, "fail: non-secret should still fail enum") +ngx.say("passed: skip_validation schema-aware hook only skips string-typed fields")