Skip to content

Commit 72a5835

Browse files
committed
Add OpenAPI 3.2 tags
1 parent 268e427 commit 72a5835

File tree

3 files changed

+84
-1
lines changed

3 files changed

+84
-1
lines changed

openapi_spec_validator/validation/keywords.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,18 @@ def __call__(self, components: SchemaPath) -> Iterator[ValidationError]:
642642
yield from self.schemas_validator(schemas)
643643

644644

645+
class OpenAPIV32TagsValidator(KeywordValidator):
646+
def __call__(self, tags: SchemaPath) -> Iterator[ValidationError]:
647+
seen: set[str] = set()
648+
for tag in tags:
649+
tag_name = (tag / "name").read_str()
650+
if tag_name in seen:
651+
yield OpenAPIValidationError(
652+
f"Duplicate tag name '{tag_name}'"
653+
)
654+
seen.add(tag_name)
655+
656+
645657
class RootValidator(KeywordValidator):
646658
@property
647659
def paths_validator(self) -> PathsValidator:
@@ -658,3 +670,15 @@ def __call__(self, spec: SchemaPath) -> Iterator[ValidationError]:
658670
if "components" in spec:
659671
components = spec / "components"
660672
yield from self.components_validator(components)
673+
674+
675+
class OpenAPIV32RootValidator(RootValidator):
676+
@property
677+
def tags_validator(self) -> OpenAPIV32TagsValidator:
678+
return cast(OpenAPIV32TagsValidator, self.registry["tags"])
679+
680+
def __call__(self, spec: SchemaPath) -> Iterator[ValidationError]:
681+
if "tags" in spec:
682+
tags = spec / "tags"
683+
yield from self.tags_validator(tags)
684+
yield from super().__call__(spec)

openapi_spec_validator/validation/validators.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ class OpenAPIV31SpecValidator(SpecValidator):
154154
class OpenAPIV32SpecValidator(SpecValidator):
155155
schema_validator = openapi_v32_schema_validator
156156
keyword_validators = {
157-
"__root__": keywords.RootValidator,
157+
"__root__": keywords.OpenAPIV32RootValidator,
158158
"components": keywords.ComponentsValidator,
159159
"content": keywords.ContentValidator,
160160
"default": keywords.OpenAPIV32ValueValidator,
@@ -168,5 +168,6 @@ class OpenAPIV32SpecValidator(SpecValidator):
168168
"responses": keywords.ResponsesValidator,
169169
"schema": keywords.OpenAPIV32SchemaValidator,
170170
"schemas": keywords.SchemasValidator,
171+
"tags": keywords.OpenAPIV32TagsValidator,
171172
}
172173
root_keywords = ["paths", "components"]

tests/integration/validation/test_validators.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,64 @@ def test_additional_operations_are_semantically_validated(self):
199199
assert len(errors) == 1
200200
assert "Path parameter 'item_id'" in errors[0].message
201201

202+
def test_top_level_duplicate_tags_are_invalid(self):
203+
spec = {
204+
"openapi": "3.2.0",
205+
"info": {
206+
"title": "Tag API",
207+
"version": "1.0.0",
208+
},
209+
"tags": [
210+
{
211+
"name": "pets",
212+
},
213+
{
214+
"name": "pets",
215+
},
216+
],
217+
"paths": {
218+
"/pets": {
219+
"get": {
220+
"responses": {
221+
"200": {
222+
"description": "ok",
223+
},
224+
},
225+
},
226+
},
227+
},
228+
}
229+
230+
errors = list(OpenAPIV32SpecValidator(spec).iter_errors())
231+
232+
assert len(errors) == 1
233+
assert errors[0].message == "Duplicate tag name 'pets'"
234+
235+
def test_operation_tags_without_root_declaration_are_valid(self):
236+
spec = {
237+
"openapi": "3.2.0",
238+
"info": {
239+
"title": "Tag API",
240+
"version": "1.0.0",
241+
},
242+
"paths": {
243+
"/pets": {
244+
"get": {
245+
"tags": ["pets", "animals"],
246+
"responses": {
247+
"200": {
248+
"description": "ok",
249+
},
250+
},
251+
},
252+
},
253+
},
254+
}
255+
256+
errors = list(OpenAPIV32SpecValidator(spec).iter_errors())
257+
258+
assert not errors
259+
202260

203261
def test_oas31_query_operation_is_not_semantically_traversed():
204262
spec = {

0 commit comments

Comments
 (0)