Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Break Versioning](https://www.taoensso.com/break-ve

## [Unreleased]

### Fixed

- JSON schema generation now correctly uses `minItems`/`maxItems` for array size predicates instead of `minLength`/`maxLength` (fixes #481) (@baweaver)

## [1.14.1] - 2025-03-03

Expand Down
23 changes: 22 additions & 1 deletion lib/dry/schema/extensions/json_schema/schema_compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ def visit_predicate(node, opts = EMPTY_HASH)
target = keys[opts[:key]]
type_opts = fetch_type_opts_for_predicate(name, rest, target)

if target[:type]&.include?("array")
if target[:type]&.include?("array") && array_size_predicate?(name) && !opts[:member]
array_type_opts = convert_array_size_predicate(name, rest)
merge_opts!(target, array_type_opts)
elsif target[:type]&.include?("array")
target[:items] ||= {}
merge_opts!(target[:items], type_opts)
else
Expand All @@ -178,6 +181,24 @@ def visit_predicate(node, opts = EMPTY_HASH)
end
end

# @api private
def array_size_predicate?(name)
name == :min_size? || name == :max_size?
end

# @api private
def convert_array_size_predicate(name, rest)
value = rest[0][1].to_i
case name
when :min_size?
{ minItems: value }
when :max_size?
{ maxItems: value }
else
{}
end
end

# @api private
def fetch_type_opts_for_predicate(name, rest, target)
type_opts = PREDICATE_TO_TYPE.fetch(name) do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

RSpec.describe "JSON Schema with array size predicates" do
before do
Dry::Schema.load_extensions(:json_schema)
end

context "with min_size? and max_size? predicates" do
let(:schema) do
Dry::Schema.JSON do
required(:users).value(:array?, min_size?: 5, max_size?: 10).each(:str?)
end
end

it "generates minItems and maxItems on array" do
json_schema = schema.json_schema

expect(json_schema[:properties][:users]).to include(
type: "array",
minItems: 5,
maxItems: 10,
items: { type: "string" }
)

expect(json_schema[:properties][:users][:items]).not_to have_key(:minLength)
expect(json_schema[:properties][:users][:items]).not_to have_key(:maxLength)
end
end

context "with string items having size predicates" do
let(:schema) do
Dry::Schema.JSON do
required(:names).value(:array?, min_size?: 2).each(:str?, min_size?: 3, max_size?: 50)
end
end

it "applies array size to array and string size to items" do
json_schema = schema.json_schema

expect(json_schema[:properties][:names]).to include(
type: "array",
minItems: 2,
items: {
type: "string",
minLength: 3,
maxLength: 50
}
)
end
end

context "with equal min and max size constraints" do
let(:schema) do
Dry::Schema.JSON do
required(:users).value(:array?, min_size?: 5, max_size?: 5).each(:str?)
end
end

it "generates correct minItems and maxItems" do
expected = {
"$schema": "http://json-schema.org/draft-06/schema#",
type: "object",
properties: {
users: {
type: "array",
minItems: 5,
maxItems: 5,
items: {
type: "string"
}
}
},
required: ["users"]
}

expect(schema.json_schema).to eq(expected)
end
end
end