diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd96f5b..edc6b17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,15 @@ 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).
+## [2.0.2-beta-1] - 2026-06-25
+
+### Fixed
+- Allow `$ref` in root-level servers; previously caused `Unexpected property: "$ref"` and `Missing required properties: [protocol]`.
+- Allow `$ref` in tag entries (root, `info`, servers, channels, operations); previously caused `Unexpected property: "$ref"`.
+- Accept `availableScopes` inside OAuth2 flows (AsyncAPI 3.0 rename of `scopes`); previously rejected as unexpected.
+- Make `scopes` optional in OAuth2 flows for 3.0 compatibility; previously required, causing false `Missing required properties: [scopes]` errors.
+- Accept `scopes` at the `oauth2` security-scheme level (valid in AsyncAPI 3.0); previously rejected as unexpected.
+
## [2.0.1] - 2026-06-09
### Changed
diff --git a/asyncapi-checks/pom.xml b/asyncapi-checks/pom.xml
index 2cbf9eb..7cfc273 100644
--- a/asyncapi-checks/pom.xml
+++ b/asyncapi-checks/pom.xml
@@ -5,7 +5,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi-asyncapi
- 2.0.1
+ 2.0.2-beta-1
../pom.xml
diff --git a/asyncapi-checks/src/test/resources/v3.1/valid-with-refs-and-oauth3.yaml b/asyncapi-checks/src/test/resources/v3.1/valid-with-refs-and-oauth3.yaml
new file mode 100644
index 0000000..5081903
--- /dev/null
+++ b/asyncapi-checks/src/test/resources/v3.1/valid-with-refs-and-oauth3.yaml
@@ -0,0 +1,80 @@
+asyncapi: "3.1.0"
+info:
+ title: Refs and OAuth3 Test API
+ version: 1.0.0
+ description: Exercises $ref in servers/tags and AsyncAPI 3.0 OAuth2 syntax.
+
+servers:
+ production:
+ $ref: '#/components/servers/production'
+ staging:
+ $ref: '#/components/servers/staging'
+
+channels:
+ orderCreated:
+ address: order/created
+ messages:
+ orderCreatedMessage:
+ name: orderCreated
+ payload:
+ type: object
+ properties:
+ orderId:
+ type: string
+
+operations:
+ onOrderCreated:
+ action: receive
+ channel:
+ $ref: '#/channels/orderCreated'
+ tags:
+ - $ref: '#/components/tags/orders'
+
+tags:
+ - $ref: '#/components/tags/orders'
+ - $ref: '#/components/tags/admin'
+
+components:
+ servers:
+ production:
+ host: broker.example.com
+ port: 5672
+ protocol: amqp
+ description: Production AMQP broker
+ tags:
+ - $ref: '#/components/tags/orders'
+
+ staging:
+ host: staging-broker.example.com
+ port: 5672
+ protocol: amqp
+ description: Staging AMQP broker
+
+ tags:
+ orders:
+ name: orders
+ description: Order-related operations
+ admin:
+ name: admin
+ description: Administrative operations
+
+ securitySchemes:
+ oauth2:
+ type: oauth2
+ description: OAuth2 security scheme
+ flows:
+ clientCredentials:
+ tokenUrl: https://auth.example.com/oauth/token
+ refreshUrl: https://auth.example.com/oauth/refresh
+ availableScopes:
+ read:orders: Read order data
+ write:orders: Create and update orders
+ authorizationCode:
+ authorizationUrl: https://auth.example.com/oauth/authorize
+ tokenUrl: https://auth.example.com/oauth/token
+ availableScopes:
+ read:orders: Read order data
+ write:orders: Create and update orders
+ scopes:
+ - read:orders
+ - write:orders
diff --git a/asyncapi-front-end/pom.xml b/asyncapi-front-end/pom.xml
index 90b7c98..3c6522a 100644
--- a/asyncapi-front-end/pom.xml
+++ b/asyncapi-front-end/pom.xml
@@ -5,7 +5,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi-asyncapi
- 2.0.1
+ 2.0.2-beta-1
../pom.xml
diff --git a/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v4/AsyncApiGrammar.java b/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v4/AsyncApiGrammar.java
index c8b0d37..ac74095 100644
--- a/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v4/AsyncApiGrammar.java
+++ b/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v4/AsyncApiGrammar.java
@@ -140,6 +140,7 @@ public static YamlGrammarBuilder create() {
private static void buildTags(YamlGrammarBuilder b) {
b.rule(TAG).is(b.firstOf(
+ REF,
b.string(),
b.object(
b.property("name", b.string()),
@@ -168,6 +169,7 @@ private static void buildSecurityDefinitions(YamlGrammarBuilder b) {
b.discriminant("type", "oauth2"),
b.property("description", DESCRIPTION),
b.mandatoryProperty("flows", FLOWS),
+ b.property("scopes", b.array(b.string())),
b.patternProperty(EXTENSION_PATTERN, b.anything()))).skip();
b.rule(OPENID_SECURITY_SCHEME).is(b.object(
b.discriminant("type", "openIdConnect"),
@@ -183,26 +185,34 @@ private static void buildSecurityDefinitions(YamlGrammarBuilder b) {
b.rule(IMPLICIT_FLOW).is(b.object(
b.mandatoryProperty("authorizationUrl", b.string()),
b.property("refreshUrl", b.string()),
- b.mandatoryProperty("scopes", b.object(
+ b.property("scopes", b.object(
+ b.patternProperty(".*", b.string()))),
+ b.property("availableScopes", b.object(
b.patternProperty(".*", b.string()))),
b.patternProperty(EXTENSION_PATTERN, b.anything())));
b.rule(PASSWORD_FLOW).is(b.object(
b.mandatoryProperty("tokenUrl", b.string()),
b.property("refreshUrl", b.string()),
- b.mandatoryProperty("scopes", b.object(
+ b.property("scopes", b.object(
+ b.patternProperty(".*", b.string()))),
+ b.property("availableScopes", b.object(
b.patternProperty(".*", b.string()))),
b.patternProperty(EXTENSION_PATTERN, b.anything())));
b.rule(CREDENTIALS_FLOW).is(b.object(
b.mandatoryProperty("tokenUrl", b.string()),
b.property("refreshUrl", b.string()),
- b.mandatoryProperty("scopes", b.object(
+ b.property("scopes", b.object(
+ b.patternProperty(".*", b.string()))),
+ b.property("availableScopes", b.object(
b.patternProperty(".*", b.string()))),
b.patternProperty(EXTENSION_PATTERN, b.anything())));
b.rule(AUTH_FLOW).is(b.object(
b.mandatoryProperty("authorizationUrl", b.string()),
b.mandatoryProperty("tokenUrl", b.string()),
b.property("refreshUrl", b.string()),
- b.mandatoryProperty("scopes", b.object(
+ b.property("scopes", b.object(
+ b.patternProperty(".*", b.string()))),
+ b.property("availableScopes", b.object(
b.patternProperty(".*", b.string()))),
b.patternProperty(EXTENSION_PATTERN, b.anything())));
b.rule(SECURITY_REQUIREMENT).is(b.object(
@@ -518,8 +528,8 @@ private static void buildServers(YamlGrammarBuilder b) {
b.rule(SERVERS).is(
b.firstOf(
- b.array(SERVER),
- b.object(b.patternProperty(".*", SERVER))));
+ b.array(b.firstOf(REF, SERVER)),
+ b.object(b.patternProperty(".*", b.firstOf(REF, SERVER)))));
}
// Info
diff --git a/asyncapi-test-tools/pom.xml b/asyncapi-test-tools/pom.xml
index ebfc5c3..6295fc7 100644
--- a/asyncapi-test-tools/pom.xml
+++ b/asyncapi-test-tools/pom.xml
@@ -5,7 +5,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi-asyncapi
- 2.0.1
+ 2.0.2-beta-1
../pom.xml
diff --git a/its/pom.xml b/its/pom.xml
index 092398a..4285990 100644
--- a/its/pom.xml
+++ b/its/pom.xml
@@ -3,7 +3,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi-asyncapi
- 2.0.1
+ 2.0.2-beta-1
../pom.xml
4.0.0
diff --git a/pom.xml b/pom.xml
index 83d9c19..a0c8f81 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi-asyncapi
- 2.0.1
+ 2.0.2-beta-1
pom
SonarAsyncAPI
diff --git a/sonar-asyncapi-plugin/pom.xml b/sonar-asyncapi-plugin/pom.xml
index 31f1d56..d17128b 100644
--- a/sonar-asyncapi-plugin/pom.xml
+++ b/sonar-asyncapi-plugin/pom.xml
@@ -5,7 +5,7 @@
org.apiaddicts.apitools.dosonarapi
dosonarapi-asyncapi
- 2.0.1
+ 2.0.2-beta-1
../pom.xml