Skip to content

Commit 92ec47d

Browse files
authored
feat: add skip_validation custom hook for value-level bypass (#103)
1 parent 9d410a7 commit 92ec47d

2 files changed

Lines changed: 167 additions & 1 deletion

File tree

lib/jsonschema.lua

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ local function codectx(schema, options)
335335
-- schema management
336336
_validators = {}, -- maps paths to local variable validators
337337
_external_resolver = options.external_resolver,
338+
_skip_validation = options.skip_validation,
338339
}, codectx_mt)
339340
self._root = self
340341
return self
@@ -618,6 +619,19 @@ generate_validator = function(ctx, schema)
618619
return ctx
619620
end
620621

622+
-- skip_validation hook: if the caller provided a predicate via
623+
-- custom.skip_validation, check it before any constraint. When it
624+
-- returns true the value is accepted as-is (useful for placeholder
625+
-- strings like secret references that will be resolved at runtime).
626+
-- The predicate receives (value, schema) so it can inspect the
627+
-- expected type of the field being validated.
628+
if ctx._root._skip_validation then
629+
local schema_ref = ctx:uservalue(schema)
630+
ctx:stmt(sformat('if %s ~= nil and %s(%s, %s) then return true end',
631+
ctx:param(1),
632+
ctx:libfunc('custom.skip_validation'), ctx:param(1), schema_ref))
633+
end
634+
621635
-- type check
622636
local tt = type(schema.type)
623637
if tt == 'string' then
@@ -1206,7 +1220,8 @@ return {
12061220
null = custom and custom.null or default_null,
12071221
match_pattern = custom and custom.match_pattern or match_pattern,
12081222
parse_ipv4 = custom and custom.parse_ipv4 or parse_ipv4,
1209-
parse_ipv6 = custom and custom.parse_ipv6 or parse_ipv6
1223+
parse_ipv6 = custom and custom.parse_ipv6 or parse_ipv6,
1224+
skip_validation = custom and custom.skip_validation or nil,
12101225
}
12111226
local name = custom and custom.name
12121227
local has_original_id

t/default.lua

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,154 @@ local pcall_ok, valid, val_err = pcall(validator, { "a", "b", "c" })
250250
assert(pcall_ok, "fail: validator threw an error: " .. tostring(valid))
251251
assert(valid, "fail: validator returned false: " .. tostring(val_err))
252252
ngx.say("passed: recursive datatype is not shared across calls")
253+
254+
----------------------------------------------------- test case 10
255+
-- skip_validation: type bypass - string value passes integer schema
256+
local skip_fn = function(val, schema)
257+
return type(val) == "string" and val:sub(1, 10) == "$secret://"
258+
end
259+
260+
rule = {
261+
type = "object",
262+
properties = {
263+
host = { type = "string" },
264+
port = { type = "integer" },
265+
enabled = { type = "boolean" },
266+
},
267+
required = { "host", "port" },
268+
}
269+
270+
validator = jsonschema.generate_validator(rule, { skip_validation = skip_fn })
271+
ok, err = validator({ host = "$secret://vault/host", port = "$secret://vault/port", enabled = "$secret://vault/flag" })
272+
assert(ok, "fail: skip_validation should bypass type checks: " .. tostring(err))
273+
ngx.say("passed: skip_validation bypasses type checks")
274+
275+
----------------------------------------------------- test case 11
276+
-- skip_validation: enum bypass
277+
rule = {
278+
type = "object",
279+
properties = {
280+
scheme = { type = "string", enum = { "http", "https" } },
281+
},
282+
}
283+
validator = jsonschema.generate_validator(rule, { skip_validation = skip_fn })
284+
ok, err = validator({ scheme = "$secret://vault/scheme" })
285+
assert(ok, "fail: skip_validation should bypass enum: " .. tostring(err))
286+
-- non-secret string should still be validated
287+
ok, err = validator({ scheme = "ftp" })
288+
assert(not ok, "fail: non-secret value should still fail enum")
289+
ngx.say("passed: skip_validation bypasses enum but normal values validated")
290+
291+
----------------------------------------------------- test case 12
292+
-- skip_validation: default values still set for nil fields
293+
rule = {
294+
type = "object",
295+
properties = {
296+
host = { type = "string" },
297+
port = { type = "integer", default = 6379 },
298+
},
299+
}
300+
validator = jsonschema.generate_validator(rule, { skip_validation = skip_fn })
301+
local conf = { host = "$secret://vault/host" }
302+
ok, err = validator(conf)
303+
assert(ok, "fail: skip_validation with defaults: " .. tostring(err))
304+
assert(conf.port == 6379, "fail: default value should still be set")
305+
ngx.say("passed: skip_validation preserves default values")
306+
307+
----------------------------------------------------- test case 13
308+
-- skip_validation: nested object properties still validated
309+
rule = {
310+
type = "object",
311+
properties = {
312+
upstream = {
313+
type = "object",
314+
properties = {
315+
host = { type = "string" },
316+
port = { type = "integer" },
317+
},
318+
required = { "host" },
319+
},
320+
},
321+
}
322+
validator = jsonschema.generate_validator(rule, { skip_validation = skip_fn })
323+
-- nested secret ref should pass
324+
ok, err = validator({ upstream = { host = "$secret://vault/host", port = "$secret://vault/port" } })
325+
assert(ok, "fail: nested skip_validation: " .. tostring(err))
326+
-- whole object as secret ref should pass
327+
ok, err = validator({ upstream = "$secret://vault/upstream" })
328+
assert(ok, "fail: object-level skip_validation: " .. tostring(err))
329+
ngx.say("passed: skip_validation works for nested objects")
330+
331+
----------------------------------------------------- test case 14
332+
-- skip_validation: array items bypass
333+
rule = {
334+
type = "object",
335+
properties = {
336+
tags = {
337+
type = "array",
338+
items = { type = "string", minLength = 3 },
339+
},
340+
},
341+
}
342+
validator = jsonschema.generate_validator(rule, { skip_validation = skip_fn })
343+
ok, err = validator({ tags = { "abc", "$secret://vault/tag" } })
344+
assert(ok, "fail: array items skip_validation: " .. tostring(err))
345+
ngx.say("passed: skip_validation works for array items")
346+
347+
----------------------------------------------------- test case 15
348+
-- skip_validation: not configured - normal validation applies
349+
rule = {
350+
type = "object",
351+
properties = {
352+
port = { type = "integer" },
353+
},
354+
}
355+
validator = jsonschema.generate_validator(rule)
356+
ok, err = validator({ port = "$secret://vault/port" })
357+
assert(not ok, "fail: without skip_validation, string should fail integer type check")
358+
ngx.say("passed: without skip_validation, normal validation applies")
359+
360+
----------------------------------------------------- test case 16
361+
-- skip_validation: schema-aware hook - only bypass for string-typed fields
362+
-- This demonstrates using the schema parameter to make smart decisions:
363+
-- secret refs in string fields bypass constraints (enum, pattern, format),
364+
-- but secret refs in non-string fields (integer/boolean) are rejected.
365+
local schema_aware_skip = function(val, schema)
366+
if type(val) ~= "string" or val:sub(1, 10) ~= "$secret://" then
367+
return false
368+
end
369+
-- Only bypass when the schema expects a string type.
370+
-- For non-string fields, do not skip — we don't allow string
371+
-- placeholders in integer/boolean/object fields at validation time.
372+
if not schema or schema.type ~= "string" then
373+
return false
374+
end
375+
return true
376+
end
377+
378+
rule = {
379+
type = "object",
380+
properties = {
381+
host = { type = "string", pattern = "^[a-z]+%.com$" },
382+
scheme = { type = "string", enum = { "http", "https" } },
383+
port = { type = "integer", minimum = 1, maximum = 65535 },
384+
enabled = { type = "boolean" },
385+
},
386+
}
387+
validator = jsonschema.generate_validator(rule, { skip_validation = schema_aware_skip })
388+
-- secret ref in string+pattern field: bypassed (schema.type == "string")
389+
ok, err = validator({ host = "$secret://vault/host" })
390+
assert(ok, "fail: schema-aware skip should bypass string+pattern: " .. tostring(err))
391+
-- secret ref in string+enum field: bypassed (schema.type == "string")
392+
ok, err = validator({ scheme = "$secret://vault/scheme" })
393+
assert(ok, "fail: schema-aware skip should bypass string+enum: " .. tostring(err))
394+
-- secret ref in integer field: NOT bypassed (schema.type == "integer")
395+
ok, err = validator({ port = "$secret://vault/port" })
396+
assert(not ok, "fail: schema-aware skip should NOT bypass integer field")
397+
-- secret ref in boolean field: NOT bypassed (schema.type == "boolean")
398+
ok, err = validator({ enabled = "$secret://vault/flag" })
399+
assert(not ok, "fail: schema-aware skip should NOT bypass boolean field")
400+
-- non-secret invalid enum value: still fails
401+
ok, err = validator({ scheme = "ftp" })
402+
assert(not ok, "fail: non-secret should still fail enum")
403+
ngx.say("passed: skip_validation schema-aware hook only skips string-typed fields")

0 commit comments

Comments
 (0)