diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index efe3046..57b0cab 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -23,10 +23,16 @@ jobs:
cache: maven
- name: Build, Test and Coverage
run: mvn -B verify --file pom.xml
+ - name: Set up JDK 21 for Sonar
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
- name: SonarCloud Analysis
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- run: mvn sonar:sonar -Dsonar.token=$SONAR_TOKEN
+ run: mvn org.sonarsource.scanner.maven:sonar-maven-plugin:3.11.0.3922:sonar -Dsonar.token=$SONAR_TOKEN
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@v4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d97cfab..4aa9ea5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.2.2-beta-1] - 2026-08-18
+
+### Added
+
+- Accept OpenAPI `3.0.4`, `3.1.1` and `3.1.2` in version dispatch and the v3/v3.1/v3.2 grammars.
+
## [1.2.1] - 2026-06-04
### Added
diff --git a/README.md b/README.md
index 852982a..8a5cd0e 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ Feel free to drop by and greet us on our GitHub discussion or Discord chat. You
## ⚙️ Features
-* Full compatibility with OpenAPI v2.0, v3.0.0, v3.0.1, v3.0.2, v3.0.3, v3.1.0 and v3.2.0
+* Full compatibility with OpenAPI v2.0, v3.0.0, v3.0.1, v3.0.2, v3.0.3, v3.0.4, v3.1.0, v3.1.1, v3.1.2 and v3.2.0

diff --git a/its/pom.xml b/its/pom.xml
index 49f6740..37277d6 100644
--- a/its/pom.xml
+++ b/its/pom.xml
@@ -5,7 +5,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi
- 1.2.1
+ 1.2.2-beta-1
../pom.xml
4.0.0
diff --git a/openapi-checks/pom.xml b/openapi-checks/pom.xml
index 4ae3c61..ae89cbb 100644
--- a/openapi-checks/pom.xml
+++ b/openapi-checks/pom.xml
@@ -5,7 +5,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi
- 1.2.1
+ 1.2.2-beta-1
../pom.xml
diff --git a/openapi-front-end/pom.xml b/openapi-front-end/pom.xml
index 9c28b76..1d6729c 100644
--- a/openapi-front-end/pom.xml
+++ b/openapi-front-end/pom.xml
@@ -5,7 +5,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi
- 1.2.1
+ 1.2.2-beta-1
../pom.xml
diff --git a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v3/OpenApi3Grammar.java b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v3/OpenApi3Grammar.java
index d275bc8..38156ed 100644
--- a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v3/OpenApi3Grammar.java
+++ b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v3/OpenApi3Grammar.java
@@ -83,7 +83,7 @@ public static YamlGrammarBuilder create() {
b.setRootRule(ROOT);
b.rule(ROOT).is(b.object(
- b.mandatoryProperty("openapi", b.firstOf("3.0.0", "3.0.1", "3.0.2", "3.0.3")),
+ b.mandatoryProperty("openapi", b.firstOf("3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4")),
b.mandatoryProperty("info", INFO),
b.property("servers", b.array(SERVER)),
b.mandatoryProperty("paths", PATHS),
diff --git a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v31/OpenApi31Grammar.java b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v31/OpenApi31Grammar.java
index b359e92..74c01ef 100644
--- a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v31/OpenApi31Grammar.java
+++ b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v31/OpenApi31Grammar.java
@@ -78,6 +78,7 @@ public enum OpenApi31Grammar implements GrammarRuleKey {
SECURITY_SCHEMES,
LINKS_COMPONENT,
CALLBACKS_COMPONENT,
+ PATH_ITEMS_COMPONENT,
SCHEMA_PROPERTIES,
DESCRIPTION;
@@ -88,10 +89,11 @@ public static YamlGrammarBuilder create() {
b.setRootRule(ROOT);
b.rule(ROOT).is(b.object(
- b.mandatoryProperty("openapi", "3.1.0"),
+ b.mandatoryProperty("openapi", b.firstOf("3.1.0", "3.1.1", "3.1.2")),
b.mandatoryProperty("info", INFO),
+ b.property("jsonSchemaDialect", b.string()),
b.property("servers", b.array(SERVER)),
- b.mandatoryProperty("paths", PATHS),
+ b.property("paths", PATHS),
b.property("webhooks", WEBHOOKS),
b.property("components", COMPONENTS),
b.property("security", b.array(SECURITY_REQUIREMENT)),
@@ -151,12 +153,14 @@ private static void buildComponents(YamlGrammarBuilder b) {
b.property("securitySchemes", SECURITY_SCHEMES),
b.property("links", LINKS_COMPONENT),
b.property("callbacks", CALLBACKS_COMPONENT),
+ b.property("pathItems", PATH_ITEMS_COMPONENT),
b.patternProperty(EXTENSION_PATTERN, b.anything())));
OpenApiGrammar.buildBaseComponentRules(b,
SCHEMAS_COMPONENT, RESPONSES_COMPONENT, PARAMETERS_COMPONENT, EXAMPLES_COMPONENT,
BODIES_COMPONENT, HEADERS_COMPONENT, SECURITY_SCHEMES, LINKS_COMPONENT, CALLBACKS_COMPONENT,
REF, SCHEMA, RESPONSE, PARAMETER, EXAMPLE, REQUEST_BODY, HEADER, SECURITY_SCHEME, LINK, CALLBACK);
b.rule(WEBHOOKS_COMPONENT).is(b.object(b.patternProperty(".*", b.firstOf(REF, WEBHOOK))));
+ b.rule(PATH_ITEMS_COMPONENT).is(b.object(b.patternProperty(".*", b.firstOf(REF, PATH))));
buildParameters(b);
buildResponses(b);
@@ -185,16 +189,35 @@ private static void buildSchema(YamlGrammarBuilder b) {
b.property("type", b.firstOf(b.string(), b.array(b.string()))),
b.property("contentMediaType", b.string()),
b.property("contentEncoding", b.string()),
+ b.property("contentSchema", b.firstOf(REF, SCHEMA)),
b.property("allOf", b.array(b.firstOf(REF, SCHEMA))),
b.property("oneOf", b.array(b.firstOf(REF, SCHEMA))),
b.property("anyOf", b.array(b.firstOf(REF, SCHEMA))),
b.property("not", b.firstOf(REF, SCHEMA)),
+ b.property("if", b.firstOf(REF, SCHEMA)),
+ b.property("then", b.firstOf(REF, SCHEMA)),
+ b.property("else", b.firstOf(REF, SCHEMA)),
+ b.property("prefixItems", b.array(b.firstOf(REF, SCHEMA))),
b.property("items", b.firstOf(REF, SCHEMA)),
+ b.property("contains", b.firstOf(REF, SCHEMA)),
+ b.property("minContains", b.integer()),
+ b.property("maxContains", b.integer()),
+ b.property("unevaluatedItems", b.firstOf(b.bool(), REF, SCHEMA)),
b.property("properties", SCHEMA_PROPERTIES),
+ b.property("patternProperties", SCHEMA_PROPERTIES),
+ b.property("propertyNames", b.firstOf(REF, SCHEMA)),
+ b.property("dependentSchemas", b.object(b.patternProperty(".*", b.firstOf(REF, SCHEMA)))),
+ b.property("dependentRequired", b.object(b.patternProperty(".*", b.array(b.string())))),
b.property("$schema", b.string()),
+ b.property("$id", b.string()),
+ b.property("$anchor", b.string()),
+ b.property("$defs", b.object(b.patternProperty(".*", b.firstOf(REF, SCHEMA)))),
+ b.property("$dynamicRef", b.string()),
+ b.property("$dynamicAnchor", b.string()),
+ b.property("$comment", b.string()),
b.property("additionalProperties", b.firstOf(b.bool(), REF, SCHEMA)),
b.property("description", DESCRIPTION),
- b.property("unevaluatedProperties", b.bool()),
+ b.property("unevaluatedProperties", b.firstOf(b.bool(), REF, SCHEMA)),
b.property("format", b.string()),
b.property("default", b.anything()),
b.property("nullable", b.bool()),
diff --git a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v32/OpenApi32Grammar.java b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v32/OpenApi32Grammar.java
index 7a6827c..3fb0e9e 100644
--- a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v32/OpenApi32Grammar.java
+++ b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v32/OpenApi32Grammar.java
@@ -236,6 +236,7 @@ private static void buildSchema(YamlGrammarBuilder b) {
b.property("type", b.firstOf(b.string(), b.array(b.string()))),
b.property("contentMediaType", b.string()),
b.property("contentEncoding", b.string()),
+ b.property("contentSchema", b.firstOf(REF, SCHEMA)),
b.property("allOf", b.array(b.firstOf(REF, SCHEMA))),
b.property("oneOf", b.array(b.firstOf(REF, SCHEMA))),
b.property("anyOf", b.array(b.firstOf(REF, SCHEMA))),
@@ -255,6 +256,7 @@ private static void buildSchema(YamlGrammarBuilder b) {
b.property("dependentSchemas", b.object(b.patternProperty(".*", b.firstOf(REF, SCHEMA)))),
b.property("dependentRequired", b.object(b.patternProperty(".*", b.array(b.string())))),
b.property("$schema", b.string()),
+ b.property("$id", b.string()),
b.property("$anchor", b.string()),
b.property("$defs", b.object(b.patternProperty(".*", b.firstOf(REF, SCHEMA)))),
b.property("$dynamicRef", b.string()),
diff --git a/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v31/JsonSchemaTest.java b/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v31/JsonSchemaTest.java
index 61a7c6f..cb5a288 100644
--- a/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v31/JsonSchemaTest.java
+++ b/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v31/JsonSchemaTest.java
@@ -23,6 +23,8 @@
import org.junit.Test;
import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode;
+import static org.assertj.core.api.Assertions.assertThat;
+
public class JsonSchemaTest extends BaseNodeTest {
@Test
@@ -48,4 +50,23 @@ public class JsonSchemaTest extends BaseNodeTest {
JsonNode param = node.at("/value");
assertEquals("http://json-schema.org/draft-07/schema#", param, "/$schema");
}
+
+ @Test
+ public void json_schema_keywords() {
+ JsonNode node = parseResource(OpenApi31Grammar.SCHEMA, "/models/v31/jsonschemakeywords.yaml");
+
+ assertThat(issues).isEmpty();
+ assertEquals("https://example.com/schemas/person", node, "/$id");
+ assertEquals("base64", node, "/properties/id/contentEncoding");
+ assertEquals("1", node, "/properties/tags/minContains");
+ }
+
+ @Test
+ public void root_without_paths_with_dialect_and_path_items() {
+ JsonNode node = parseResource(OpenApi31Grammar.ROOT, "/models/v31/webhooksonly.yaml");
+
+ assertThat(issues).isEmpty();
+ assertEquals("https://spec.openapis.org/oas/3.1/dialect/base", node, "/jsonSchemaDialect");
+ assertMissing(node.at("/paths"));
+ }
}
diff --git a/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/openapi/parser/OpenApiParserTest.java b/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/openapi/parser/OpenApiParserTest.java
index 643f3b3..b2d90a4 100644
--- a/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/openapi/parser/OpenApiParserTest.java
+++ b/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/openapi/parser/OpenApiParserTest.java
@@ -30,6 +30,7 @@
public class OpenApiParserTest {
private static final OpenApiConfiguration CONFIG = new OpenApiConfiguration(StandardCharsets.UTF_8, false);
+ private static final OpenApiConfiguration STRICT = new OpenApiConfiguration(StandardCharsets.UTF_8, true);
private static final String MINIMAL_V2 =
"swagger: \"2.0\"\n" +
@@ -45,6 +46,14 @@ public class OpenApiParserTest {
" version: 1.0.0\n" +
"paths: {}";
+ private static String minimalOpenApi(String version) {
+ return "openapi: \"" + version + "\"\n" +
+ "info:\n" +
+ " title: Test\n" +
+ " version: 1.0.0\n" +
+ "paths: {}";
+ }
+
@Test
public void create_v2_parser_parses_swagger_doc() {
YamlParser parser = OpenApiParser.createV2(CONFIG);
@@ -63,6 +72,30 @@ public void create_v3_parser_parses_openapi_doc() {
assertThat(root.at("/openapi").getTokenValue()).isEqualTo("3.0.0");
}
+ @Test
+ public void create_v3_parser_accepts_openapi_304() {
+ YamlParser parser = OpenApiParser.createV3(STRICT);
+ JsonNode root = parser.parse(minimalOpenApi("3.0.4"));
+ assertThat(root).isNotNull();
+ assertThat(root.at("/openapi").getTokenValue()).isEqualTo("3.0.4");
+ }
+
+ @Test
+ public void create_v31_parser_accepts_openapi_311() {
+ YamlParser parser = OpenApiParser.createV31(STRICT);
+ JsonNode root = parser.parse(minimalOpenApi("3.1.1"));
+ assertThat(root).isNotNull();
+ assertThat(root.at("/openapi").getTokenValue()).isEqualTo("3.1.1");
+ }
+
+ @Test
+ public void create_v31_parser_accepts_openapi_312() {
+ YamlParser parser = OpenApiParser.createV31(STRICT);
+ JsonNode root = parser.parse(minimalOpenApi("3.1.2"));
+ assertThat(root).isNotNull();
+ assertThat(root.at("/openapi").getTokenValue()).isEqualTo("3.1.2");
+ }
+
@Test
public void create_v31_parser_returns_non_null() {
YamlParser parser = OpenApiParser.createV31(CONFIG);
diff --git a/openapi-front-end/src/test/resources/models/v31/jsonschemakeywords.yaml b/openapi-front-end/src/test/resources/models/v31/jsonschemakeywords.yaml
new file mode 100644
index 0000000..fc2f7e0
--- /dev/null
+++ b/openapi-front-end/src/test/resources/models/v31/jsonschemakeywords.yaml
@@ -0,0 +1,46 @@
+$schema: "https://json-schema.org/draft/2020-12/schema"
+$id: "https://example.com/schemas/person"
+$anchor: "person"
+$comment: "Exercises JSON Schema keywords supported by OpenAPI 3.1"
+$defs:
+ name:
+ type: string
+type: [object, "null"]
+properties:
+ id:
+ type: [string, "null"]
+ contentEncoding: base64
+ contentMediaType: application/json
+ contentSchema:
+ type: object
+ tags:
+ type: array
+ prefixItems:
+ - type: string
+ - type: integer
+ items:
+ type: string
+ contains:
+ type: string
+ minContains: 1
+ maxContains: 5
+ unevaluatedItems: false
+patternProperties:
+ "^x-meta-":
+ type: string
+propertyNames:
+ pattern: "^[a-z]+$"
+dependentSchemas:
+ creditCard:
+ required: [billingAddress]
+dependentRequired:
+ creditCard: [billingAddress]
+if:
+ properties:
+ kind:
+ const: admin
+then:
+ required: [permissions]
+else:
+ required: [role]
+unevaluatedProperties: false
diff --git a/openapi-front-end/src/test/resources/models/v31/webhooksonly.yaml b/openapi-front-end/src/test/resources/models/v31/webhooksonly.yaml
new file mode 100644
index 0000000..b984db4
--- /dev/null
+++ b/openapi-front-end/src/test/resources/models/v31/webhooksonly.yaml
@@ -0,0 +1,18 @@
+openapi: "3.1.1"
+info:
+ title: Webhooks only
+ version: 1.0.0
+jsonSchemaDialect: "https://spec.openapis.org/oas/3.1/dialect/base"
+webhooks:
+ newPet:
+ post:
+ responses:
+ '200':
+ description: ok
+components:
+ pathItems:
+ petItem:
+ get:
+ responses:
+ '200':
+ description: ok
diff --git a/openapi-test-tools/pom.xml b/openapi-test-tools/pom.xml
index cf684f6..8ed2d72 100644
--- a/openapi-test-tools/pom.xml
+++ b/openapi-test-tools/pom.xml
@@ -5,7 +5,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi
- 1.2.1
+ 1.2.2-beta-1
../pom.xml
diff --git a/pom.xml b/pom.xml
index 5719259..c47f9ce 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi
- 1.2.1
+ 1.2.2-beta-1
pom
SonarOpenAPI
diff --git a/sonar-openapi-plugin/pom.xml b/sonar-openapi-plugin/pom.xml
index 957db01..e365ba6 100644
--- a/sonar-openapi-plugin/pom.xml
+++ b/sonar-openapi-plugin/pom.xml
@@ -5,7 +5,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi
- 1.2.1
+ 1.2.2-beta-1
../pom.xml
diff --git a/sonar-openapi-plugin/src/main/java/org/apiaddicts/apitools/dosonarapi/plugin/OpenApiAnalyzer.java b/sonar-openapi-plugin/src/main/java/org/apiaddicts/apitools/dosonarapi/plugin/OpenApiAnalyzer.java
index 8ed5257..5f4fe9d 100644
--- a/sonar-openapi-plugin/src/main/java/org/apiaddicts/apitools/dosonarapi/plugin/OpenApiAnalyzer.java
+++ b/sonar-openapi-plugin/src/main/java/org/apiaddicts/apitools/dosonarapi/plugin/OpenApiAnalyzer.java
@@ -112,7 +112,13 @@ public void scanFiles() {
private static boolean isV3Version(JsonNode openapiNode) {
if (openapiNode.isMissing()) return false;
String v = openapiNode.getTokenValue();
- return "3.0.0".equals(v) || "3.0.1".equals(v) || "3.0.2".equals(v) || "3.0.3".equals(v);
+ return "3.0.0".equals(v) || "3.0.1".equals(v) || "3.0.2".equals(v) || "3.0.3".equals(v) || "3.0.4".equals(v);
+ }
+
+ private static boolean isV31Version(JsonNode openapiNode) {
+ if (openapiNode.isMissing()) return false;
+ String v = openapiNode.getTokenValue();
+ return "3.1.0".equals(v) || "3.1.1".equals(v) || "3.1.2".equals(v);
}
private YamlParser selectParser(boolean isV2, boolean isV3, boolean isV31, boolean isV32) {
@@ -133,7 +139,7 @@ private void scanFile(InputFile inputFile) {
boolean isV2 = !rootNode.at("/swagger").isMissing();
JsonNode openapiNode = rootNode.at("/openapi");
boolean isV3 = isV3Version(openapiNode);
- boolean isV31 = !openapiNode.isMissing() && openapiNode.getTokenValue().equals("3.1.0");
+ boolean isV31 = isV31Version(openapiNode);
boolean isV32 = !openapiNode.isMissing() && openapiNode.getTokenValue().equals("3.2.0");
YamlParser targetParser = selectParser(isV2, isV3, isV31, isV32);
if (targetParser == null) return;