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: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ 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-3] - 2026-07-15
## [2.0.2-beta-3] - 2026-07-21

### Added
- **AAR050InfoTitleRequiredCheck**: The `info.title` field must exist and not be empty, so the AsyncAPI title reliably identifies the messaging contract (BUG / MAJOR).
- **AAR056AvroSchemaFormatCheck**: Wherever `schemaFormat` appears in the document (message-level in v2, `message.payload` Multi-Format Schema Object in v3, or a `components.schemas` entry) and indicates Avro, it must be exactly `application/vnd.apache.avro;version=1.9.0`, the standard version used across the Style Guide examples (BUG / MAJOR).

## [2.0.2-beta-2] - 2026-06-25

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ This plugin is supported by SonarQube versions greater or equal to `6.7.4`
- **AAR042MessageIdentifierCheck**: It is recommended to have a unique identifier per message.
- **AAR043SecurityChannelCheck**: It is recommended to add the security scheme to be used to each channel.
- **AAR050InfoTitleRequiredCheck**: The info.title field must exist and not be empty.
- **AAR056AvroSchemaFormatCheck**: When `schemaFormat` indicates Avro, it must be exactly `application/vnd.apache.avro;version=1.9.0`.

## 💛 Sponsors
<img src="https://apiaddicts.cloudappi.net/web/image/4248/LOGOCloudappi2020Versiones-01.png" alt="cloudappi" width="150"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ public static List<Class<?>> getSchemasChecks() {
AAR046AvroRecordDocCheck.class,
AAR047AvroFieldDocCheck.class,
AAR048AvroNameNomenclatureCheck.class,
AAR049AvroDefaultNullCheck.class
AAR049AvroDefaultNullCheck.class,
AAR056AvroSchemaFormatCheck.class
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package apiquality.sonar.asyncapi.checks.schemas;

import com.google.common.collect.Sets;
import com.sonar.sslr.api.AstNodeType;
import org.sonar.check.Rule;
import org.apiaddicts.apitools.dosonarapi.api.v4.AsyncApiGrammar;
import apiquality.sonar.asyncapi.checks.BaseCheck;
import apiquality.sonar.asyncapi.utils.AvroUtils;
import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode;

import java.util.Map;
import java.util.Set;

@Rule(key = AAR056AvroSchemaFormatCheck.CHECK_KEY)
public class AAR056AvroSchemaFormatCheck extends BaseCheck {
public static final String CHECK_KEY = "AAR056";
private static final String ERROR_KEY = "AAR056.error";

private static final String SCHEMA_FORMAT_KEY = "schemaFormat";
private static final String EXPECTED_SCHEMA_FORMAT = "application/vnd.apache.avro;version=1.9.0";

@Override
public Set<AstNodeType> subscribedKinds() {
return Sets.newHashSet(AsyncApiGrammar.ROOT);
}

@Override
protected void visitNode(JsonNode rootNode) {
visit(rootNode);
}

private void visit(JsonNode node) {
if (node == null || node.isMissing() || node.isNull()) {
return;
}
if (node.isArray()) {
for (JsonNode element : node.elements()) {
visit(element);
}
return;
}
if (!node.isObject()) {
return;
}
for (Map.Entry<String, JsonNode> entry : node.propertyMap().entrySet()) {
if (SCHEMA_FORMAT_KEY.equals(entry.getKey())) {
validateSchemaFormat(entry.getValue());
}
visit(entry.getValue());
}
}

private void validateSchemaFormat(JsonNode schemaFormatNode) {
if (schemaFormatNode == null || schemaFormatNode.isMissing() || schemaFormatNode.isNull()) {
return;
}
if (schemaFormatNode.isObject() || schemaFormatNode.isArray()) {
return;
}

String value = schemaFormatNode.stringValue();
if (value == null || !value.contains(AvroUtils.AVRO_SCHEMA_FORMAT_PREFIX)) {
return;
}

if (!EXPECTED_SCHEMA_FORMAT.equals(value)) {
addIssue(CHECK_KEY, translate(ERROR_KEY, value, EXPECTED_SCHEMA_FORMAT), schemaFormatNode.key());
}
}
}
1 change: 1 addition & 0 deletions src/main/resources/messages/errors.properties
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ AAR047.error=Avro field should include the doc attribute with a description
AAR048.error=Avro name must start with a letter or underscore, and can only contain letters, numbers and underscores
AAR049.error=Optional Avro fields (union with null) must declare their default value as null
AAR050.error=The info object must contain a non-empty title field
AAR056.error=The ''schemaFormat'' value ''{0}'' must be exactly ''{1}'' when the payload uses Avro
3 changes: 2 additions & 1 deletion src/main/resources/messages/errors_es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ AAR046.error=El registro Avro debería incluir el campo doc con una descripción
AAR047.error=El campo Avro debería incluir el atributo doc con una descripción
AAR048.error=El nombre Avro debe empezar con una letra o guion bajo, y solo puede contener letras, números y guiones bajos
AAR049.error=Los campos opcionales de Avro (unión con null) deben declarar su valor por defecto como null
AAR050.error=El objeto info debe contener un campo title no vacío
AAR050.error=El objeto info debe contener un campo title no vacío
AAR056.error=El valor de ''schemaFormat'' ''{0}'' debe ser exactamente ''{1}'' cuando el payload usa Avro
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<p>When a message payload (AsyncAPI 2) or a Multi-Format Schema Object (AsyncAPI 3, on <code>message.payload</code> or a <code>components.schemas</code> entry) declares an Avro <code>schemaFormat</code> — i.e. its value contains <code>application/vnd.apache.avro</code> — that value must be exactly <code>application/vnd.apache.avro;version=1.9.0</code>.</p>
<p>Version <code>1.9.0</code> is the standard Avro schema version and appears in every example throughout the Style Guide. Any other version string, or a missing version, is reported as an issue. <code>schemaFormat</code> values that are not Avro at all (e.g. a JSON Schema <code>schemaFormat</code>) are out of scope for this rule.</p>
<h2>Noncompliant Code Example (AsyncAPI 2)</h2>
<pre>
asyncapi: '2.6.0'
info:
title: Carga API
version: '1.0.0'
channels:
carga:
subscribe:
message:
schemaFormat: 'application/vnd.apache.avro;version=1.8.0'
payload:
type: record
name: CargaValue
namespace: com.example.avro
fields:
- name: id
type: string
</pre>
<h2>Compliant Solution (AsyncAPI 2)</h2>
<pre>
asyncapi: '2.6.0'
info:
title: Carga API
version: '1.0.0'
channels:
carga:
subscribe:
message:
schemaFormat: 'application/vnd.apache.avro;version=1.9.0'
payload:
type: record
name: CargaValue
namespace: com.example.avro
fields:
- name: id
type: string
</pre>
<h2>Noncompliant Code Example (AsyncAPI 3)</h2>
<pre>
asyncapi: 3.0.0
info:
title: Carga API
version: 1.0.0
channels:
carga:
address: carga
messages:
CargaMessage:
payload:
schemaFormat: 'application/vnd.apache.avro;version=1.8.0'
schema:
type: record
name: CargaValue
namespace: com.example.avro
fields:
- name: id
type: string
</pre>
<h2>Compliant Solution (AsyncAPI 3)</h2>
<pre>
asyncapi: 3.0.0
info:
title: Carga API
version: 1.0.0
channels:
carga:
address: carga
messages:
CargaMessage:
payload:
schemaFormat: 'application/vnd.apache.avro;version=1.9.0'
schema:
type: record
name: CargaValue
namespace: com.example.avro
fields:
- name: id
type: string
</pre>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"title": "AAR056 - AvroSchemaFormat - The schemaFormat must be application/vnd.apache.avro;version=1.9.0",
"type": "BUG",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5mn"
},
"tags": [
"schemas"
],
"defaultSeverity": "MAJOR"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<p>Cuando el payload de un mensaje (AsyncAPI 2) o un Multi-Format Schema Object (AsyncAPI 3, en <code>message.payload</code> o en una entrada de <code>components.schemas</code>) declara un <code>schemaFormat</code> de Avro — es decir, su valor contiene <code>application/vnd.apache.avro</code> — ese valor debe ser exactamente <code>application/vnd.apache.avro;version=1.9.0</code>.</p>
<p>La versión <code>1.9.0</code> es la versión estándar de Avro y aparece en todos los ejemplos de la Guía de Estilo. Cualquier otra versión, o la ausencia de versión, se reporta como incidencia. Los valores de <code>schemaFormat</code> que no sean de Avro (por ejemplo, un <code>schemaFormat</code> de JSON Schema) quedan fuera del alcance de esta regla.</p>
<h2>Ejemplo Noncompliant (AsyncAPI 2)</h2>
<pre>
asyncapi: '2.6.0'
info:
title: Carga API
version: '1.0.0'
channels:
carga:
subscribe:
message:
schemaFormat: 'application/vnd.apache.avro;version=1.8.0'
payload:
type: record
name: CargaValue
namespace: com.example.avro
fields:
- name: id
type: string
</pre>
<h2>Solución Compliant (AsyncAPI 2)</h2>
<pre>
asyncapi: '2.6.0'
info:
title: Carga API
version: '1.0.0'
channels:
carga:
subscribe:
message:
schemaFormat: 'application/vnd.apache.avro;version=1.9.0'
payload:
type: record
name: CargaValue
namespace: com.example.avro
fields:
- name: id
type: string
</pre>
<h2>Ejemplo Noncompliant (AsyncAPI 3)</h2>
<pre>
asyncapi: 3.0.0
info:
title: Carga API
version: 1.0.0
channels:
carga:
address: carga
messages:
CargaMessage:
payload:
schemaFormat: 'application/vnd.apache.avro;version=1.8.0'
schema:
type: record
name: CargaValue
namespace: com.example.avro
fields:
- name: id
type: string
</pre>
<h2>Solución Compliant (AsyncAPI 3)</h2>
<pre>
asyncapi: 3.0.0
info:
title: Carga API
version: 1.0.0
channels:
carga:
address: carga
messages:
CargaMessage:
payload:
schemaFormat: 'application/vnd.apache.avro;version=1.9.0'
schema:
type: record
name: CargaValue
namespace: com.example.avro
fields:
- name: id
type: string
</pre>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"title": "AAR056 - AvroSchemaFormat - El schemaFormat debe ser application/vnd.apache.avro;version=1.9.0",
"type": "BUG",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5mn"
},
"tags": [
"schemas"
],
"defaultSeverity": "MAJOR"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.sonar.samples.asyncapi.checks.schemas;

import org.junit.Before;
import org.junit.Test;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.RuleType;
import org.sonar.samples.asyncapi.BaseCheckTest;

import apiquality.sonar.asyncapi.checks.schemas.AAR056AvroSchemaFormatCheck;

public class AAR056AvroSchemaFormatCheckTest extends BaseCheckTest {

@Before
public void init() {
ruleName = "AAR056";
check = new AAR056AvroSchemaFormatCheck();
v2Path = getV2Path("schemas");
v3Path = getV3Path("schemas");
v31Path = getV31Path("schemas");
}

@Test
public void verifyV2Valid() {
verifyV2("valid.yaml");
}

@Test
public void verifyV2WrongVersion() {
verifyV2("wrong-version.yaml");
}

@Test
public void verifyV2NonAvroSchemaFormat() {
verifyV2("non-avro-schema-format.yaml");
}

@Test
public void verifyV3Valid() {
verifyV3("valid.yaml");
}

@Test
public void verifyV3WrongVersion() {
verifyV3("wrong-version.yaml");
}

@Test
public void verifyV3NonAvroSchemaFormat() {
verifyV3("non-avro-schema-format.yaml");
}

@Test
public void verifyV31Valid() {
verifyV31("valid.yaml");
}

@Test
public void verifyV31WrongVersion() {
verifyV31("wrong-version.yaml");
}

@Override
public void verifyRule() {
assertRuleProperties("AAR056 - AvroSchemaFormat - The schemaFormat must be application/vnd.apache.avro;version=1.9.0", RuleType.BUG, Severity.MAJOR, tags("schemas"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
asyncapi: '2.6.0'
info:
title: Avro Schema Format Non Avro V2
version: '1.0.0'
channels:
carga:
subscribe:
operationId: receiveCarga
message:
schemaFormat: 'application/schema+json;version=draft-07'
payload:
type: object
properties:
id:
type: string
Loading