Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 108 additions & 1 deletion lib/resty/openapi_validator/body.lua
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,105 @@ local function find_body_schema_for_content_type(route, content_type)
end


-- Check if a schema (or its allOf sub-schemas) declares the discriminator
-- property with an enum that contains the given value.
local function branch_matches_discriminator_enum(branch, prop_name, value)
local function check_props(s)
local p = s.properties and s.properties[prop_name]
if p and p.enum then
for _, v in ipairs(p.enum) do
if v == value then
return true
end
end
end
return false
end

if check_props(branch) then
return true
end

if branch.allOf then
for _, sub in ipairs(branch.allOf) do
if check_props(sub) then
return true
end
end
end

return false
end


-- Find the branch whose _ref matches the mapping target.
local function find_branch_by_mapping(branches, mapping, value)
local target_ref = mapping[value]
if not target_ref then
return nil
end

for _, branch in ipairs(branches) do
if branch._ref == target_ref then
return branch
end
if branch.allOf then
for _, sub in ipairs(branch.allOf) do
if sub._ref == target_ref then
return branch
end
end
end
end

return nil
end


-- Resolve an OpenAPI discriminator to select the correct oneOf/anyOf branch.
-- Returns (selected_schema, nil) on success, or (nil, error_string) on failure.
-- Returns (nil, nil) when the schema has no discriminator.
local function resolve_discriminator(schema, body_data)
local disc = schema.discriminator
if not disc or not disc.propertyName then
return nil, nil
end

local prop_name = disc.propertyName
local branches = schema.oneOf or schema.anyOf
if not branches then
return nil, nil
end

if type(body_data) ~= "table" then
return nil, "discriminator property '" .. prop_name .. "' is missing"
end

local value = body_data[prop_name]
if value == nil then
return nil, "discriminator property '" .. prop_name .. "' is missing"
end

-- try mapping-based lookup first (uses _ref annotations from ref resolver)
if disc.mapping then
local branch = find_branch_by_mapping(branches, disc.mapping, value)
if branch then
return branch, nil
end
end

-- fallback: match by enum on the discriminator property
for _, branch in ipairs(branches) do
if branch_matches_discriminator_enum(branch, prop_name, value) then
return branch, nil
end
end

return nil, "discriminator value '" .. tostring(value)
.. "' does not match any schema"
end


-- Check for readOnly properties present in the request body data.
local function check_readonly_properties(data, schema, errs)
if type(data) ~= "table" or type(schema) ~= "table" then
Expand Down Expand Up @@ -297,7 +396,15 @@ function _M.validate(route, body_str, content_type, opts)
check_readonly_properties(body_data, schema, errs)
end

local validator = get_validator(schema)
local effective_schema = schema
local disc_schema, disc_err = resolve_discriminator(schema, body_data)
if disc_err then
tab_insert(errs, errors.new("body", nil, disc_err))
elseif disc_schema then
effective_schema = disc_schema
end

local validator = get_validator(effective_schema)
if validator then
local ok, err = validator(body_data)
if not ok then
Expand Down
1 change: 1 addition & 0 deletions lib/resty/openapi_validator/refs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ function _M.resolve(spec)
return nil, err
end

resolved._ref = ref
registry[pointer] = resolved
resolving[pointer] = nil

Expand Down
88 changes: 88 additions & 0 deletions t/conformance/test_issue201.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env resty
--- Conformance test ported from kin-openapi issue201_test.go
-- Tests duplicate path templates with different methods and overlapping path segments.
dofile("t/lib/test_bootstrap.lua")

local T = require("test_helper")
local cjson = require("cjson.safe")
local ov = require("resty.openapi_validator")

local spec = cjson.encode({
openapi = "3.0.0",
info = { title = "Sample API", version = "1.0.0" },
paths = {
["/users/{id}"] = {
get = {
parameters = {
{ name = "id", ["in"] = "path", required = true,
schema = { type = "string" } },
},
responses = { ["200"] = { description = "OK" } },
},
post = {
parameters = {
{ name = "id", ["in"] = "path", required = true,
schema = { type = "string" } },
},
requestBody = {
required = true,
content = {
["application/json"] = {
schema = {
type = "object",
required = { "name" },
properties = {
name = { type = "string" },
},
},
},
},
},
responses = { ["200"] = { description = "OK" } },
},
},
},
})

local v = ov.compile(spec)
assert(v, "compile failed")

T.describe("issue201: GET /users/123 (valid)", function()
local ok, err = v:validate_request({
method = "GET",
path = "/users/123",
})
T.ok(ok, "should pass: " .. tostring(err))
end)

T.describe("issue201: POST /users/123 with valid body (valid)", function()
local ok, err = v:validate_request({
method = "POST",
path = "/users/123",
body = '{"name": "alice"}',
content_type = "application/json",
headers = { ["content-type"] = "application/json" },
})
T.ok(ok, "should pass: " .. tostring(err))
end)

T.describe("issue201: POST /users/123 missing required body field (fail)", function()
local ok, err = v:validate_request({
method = "POST",
path = "/users/123",
body = '{}',
content_type = "application/json",
headers = { ["content-type"] = "application/json" },
})
T.ok(not ok, "should fail - missing required field 'name'")
end)

T.describe("issue201: GET /users/abc string id (valid)", function()
local ok, err = v:validate_request({
method = "GET",
path = "/users/abc",
})
T.ok(ok, "should pass: " .. tostring(err))
end)

T.done()
149 changes: 149 additions & 0 deletions t/conformance/test_issue639.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env resty
--- Conformance test ported from kin-openapi issue639_test.go
-- Tests request body decode edge cases: empty objects, optional bodies,
-- additional properties, and nested object validation.
dofile("t/lib/test_bootstrap.lua")

local T = require("test_helper")
local cjson = require("cjson.safe")
local ov = require("resty.openapi_validator")

local spec = cjson.encode({
openapi = "3.0.0",
info = { title = "Sample API", version = "1.0.0" },
paths = {
["/items"] = {
post = {
requestBody = {
required = false,
content = {
["application/json"] = {
schema = {
type = "object",
properties = {
name = { type = "string" },
count = { type = "integer" },
metadata = {
type = "object",
properties = {
tags = {
type = "array",
items = { type = "string" },
},
nested = {
type = "object",
properties = {
level = { type = "integer" },
},
},
},
},
},
},
},
},
},
responses = { ["200"] = { description = "OK" } },
},
},
["/strict"] = {
post = {
requestBody = {
required = true,
content = {
["application/json"] = {
schema = {
type = "object",
required = { "id" },
properties = {
id = { type = "integer" },
label = { type = "string" },
},
},
},
},
},
responses = { ["200"] = { description = "OK" } },
},
},
},
})

local v = ov.compile(spec)
assert(v, "compile failed")

T.describe("issue639: empty object with only optional properties (valid)", function()
local ok, err = v:validate_request({
method = "POST",
path = "/items",
body = "{}",
content_type = "application/json",
headers = { ["content-type"] = "application/json" },
})
T.ok(ok, "should pass: " .. tostring(err))
end)

T.describe("issue639: no body when body is not required (valid)", function()
local ok, err = v:validate_request({
method = "POST",
path = "/items",
})
T.ok(ok, "should pass: " .. tostring(err))
end)

T.describe("issue639: object with all fields (valid)", function()
local ok, err = v:validate_request({
method = "POST",
path = "/items",
body = '{"name": "widget", "count": 5}',
content_type = "application/json",
headers = { ["content-type"] = "application/json" },
})
T.ok(ok, "should pass: " .. tostring(err))
end)

T.describe("issue639: object with additional properties (valid - no restriction)", function()
local ok, err = v:validate_request({
method = "POST",
path = "/items",
body = '{"name": "widget", "extra_field": "hello", "another": 42}',
content_type = "application/json",
headers = { ["content-type"] = "application/json" },
})
T.ok(ok, "should pass: " .. tostring(err))
end)

T.describe("issue639: deeply nested valid object (valid)", function()
local ok, err = v:validate_request({
method = "POST",
path = "/items",
body = '{"name": "widget", "metadata": {"tags": ["a", "b"], "nested": {"level": 3}}}',
content_type = "application/json",
headers = { ["content-type"] = "application/json" },
})
T.ok(ok, "should pass: " .. tostring(err))
end)

T.describe("issue639: missing required field in /strict (fail)", function()
local ok, err = v:validate_request({
method = "POST",
path = "/strict",
body = '{"label": "test"}',
content_type = "application/json",
headers = { ["content-type"] = "application/json" },
})
T.ok(not ok, "should fail - missing required field 'id'")
end)

T.describe("issue639: valid required field in /strict (valid)", function()
local ok, err = v:validate_request({
method = "POST",
path = "/strict",
body = '{"id": 1, "label": "test"}',
content_type = "application/json",
headers = { ["content-type"] = "application/json" },
})
T.ok(ok, "should pass: " .. tostring(err))
end)

T.done()
Loading
Loading