From 081987a52121c2f6e99da67954adbd3d3c6a3f8a Mon Sep 17 00:00:00 2001 From: Kelley Reynolds Date: Fri, 20 Feb 2026 11:18:09 -0500 Subject: [PATCH] Add validation for duplicate operationId values This fix adds validation to detect when the same operationId is used multiple times across different operations in the same OpenAPI document. According to the OpenAPI specification, operationId should be unique across all operations in a document. The validation is implemented in the Paths factory validate method, which collects all operationId values from all operations and reports any duplicates found. Changes: - Added validate_operation_ids method to NodeFactory::Paths - Added test cases for unique and duplicate operationId scenarios --- lib/openapi3_parser/node_factory/paths.rb | 26 +++++++ .../node_factory/openapi_spec.rb | 72 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/lib/openapi3_parser/node_factory/paths.rb b/lib/openapi3_parser/node_factory/paths.rb index 715e064d..8d2074a5 100644 --- a/lib/openapi3_parser/node_factory/paths.rb +++ b/lib/openapi3_parser/node_factory/paths.rb @@ -38,6 +38,7 @@ def build_node(data, node_context) def validate(validatable) paths = validatable.input.keys.grep_v(NodeFactory::EXTENSION_REGEX) validate_paths(validatable, paths) + validate_operation_ids(validatable) end def validate_paths(validatable, paths) @@ -66,6 +67,31 @@ def conflicting_paths(paths) grouped_paths.select { |group| group.size > 1 }.flatten end + + def validate_operation_ids(validatable) + operation_ids = [] + + validatable.input.each_value do |path_item| + next unless path_item.is_a?(Hash) + + %w[get put post delete options head patch trace].each do |method| + operation = path_item[method] + next unless operation.is_a?(Hash) + + operation_id = operation["operationId"] + operation_ids << operation_id if operation_id + end + end + + counts = operation_ids.each_with_object(Hash.new(0)) { |id, hash| hash[id] += 1 } + dupes = counts.select { |_id, count| count > 1 }.keys + + return if dupes.empty? + + validatable.add_error( + "Duplicate operationId values: #{dupes.sort.join(', ')}" + ) + end end end end diff --git a/spec/lib/openapi3_parser/node_factory/openapi_spec.rb b/spec/lib/openapi3_parser/node_factory/openapi_spec.rb index 0bddbe13..f2e6f764 100644 --- a/spec/lib/openapi3_parser/node_factory/openapi_spec.rb +++ b/spec/lib/openapi3_parser/node_factory/openapi_spec.rb @@ -59,6 +59,78 @@ end end + describe "validating operationIds" do + it "is valid when operationIds are unique" do + factory_context = create_node_factory_context( + minimal_openapi_definition.merge( + "paths" => { + "/pets" => { + "get" => { + "operationId" => "listPets", + "responses" => { "200" => { "description" => "Success" } } + } + }, + "/pets/{id}" => { + "get" => { + "operationId" => "getPet", + "responses" => { "200" => { "description" => "Success" } } + } + } + } + ) + ) + expect(described_class.new(factory_context)).to be_valid + end + + it "is valid when some operations have no operationId" do + factory_context = create_node_factory_context( + minimal_openapi_definition.merge( + "paths" => { + "/pets" => { + "get" => { + "operationId" => "listPets", + "responses" => { "200" => { "description" => "Success" } } + } + }, + "/pets/{id}" => { + "get" => { + "responses" => { "200" => { "description" => "Success" } } + } + } + } + ) + ) + expect(described_class.new(factory_context)).to be_valid + end + + it "is invalid when operationIds are duplicated" do + factory_context = create_node_factory_context( + minimal_openapi_definition.merge( + "paths" => { + "/pets" => { + "get" => { + "operationId" => "getPets", + "responses" => { "200" => { "description" => "Success" } } + } + }, + "/pets/{id}" => { + "get" => { + "operationId" => "getPets", + "responses" => { "200" => { "description" => "Success" } } + } + } + } + ) + ) + + instance = described_class.new(factory_context) + expect(instance).not_to be_valid + expect(instance) + .to have_validation_error("#/paths") + .with_message("Duplicate operationId values: getPets") + end + end + describe "default values for servers" do it "contains a basic root server when servers input is nil" do node = create_node(minimal_openapi_definition.merge({ "servers" => nil }))