9/29/2025

ANTLR 4 into a Quarkus app

 

Using ANTLR 4 with Quarkus: A Practical Guide

Why combine ANTLR + Quarkus?

  • Quarkus gives you a fast, cloud/native Java framework with REST, live reload, native image, etc.

  • ANTLR is a powerful parser generator to define grammars and generate parsers/lexers.

  • Together, you can embed a DSL, query language, rule engine, or expression evaluator into a Quarkus microservice.

Challenges / caveats to be aware of:

  • You must integrate ANTLR’s code generation into your build so that generated sources are compiled by Quarkus.

  • In Quarkus dev mode, some Maven plugins (like antlr4-maven-plugin) may not run automatically; you might need to explicitly run generate-sources first. Lightrun

  • Be mindful of version mismatches between ANTLR tool/grammar version and runtime. Quarkus has historically shipped with a specific ANTLR runtime version. GitHub+1


Example: Expression Parsing via REST

Let’s build a simple Quarkus app that:

  1. Accepts a mathematical expression via REST (e.g. "2 + 3 * 4")

  2. Uses ANTLR-generated parser to parse and evaluate the expression

  3. Returns the result


Project Structure

quarkus-antlr-example/ ├── pom.xml ├── src │ ├── main │ │ ├── antlr4 │ │ │ └── Expr.g4 │ │ └── java │ │ └── com/example │ │ ├── ExpressionEvaluator.java │ │ └── ExpressionResource.java │ └── test │ └── java │ └── com/example │ └── ExpressionResourceTest.java

Grammar: Expr.g4

Place under src/main/antlr4:

grammar Expr; prog: expr EOF ; expr : expr op=('*'|'/') expr # MulDiv | expr op=('+'|'-') expr # AddSub | INT # Int | '(' expr ')' # Parens ; INT : [0-9]+ ; WS : [ \t\r\n]+ -> skip ;

This defines a simple arithmetic grammar with precedence (multiplication/division higher than addition/subtraction).


pom.xml

Here’s a minimal pom.xml configured to run ANTLR code generation and build a Quarkus app:

<project xmlns="http://maven.apache.org/POM/4.0.0" ...> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>quarkus-antlr-example</artifactId> <version>1.0.0-SNAPSHOT</version> <properties> <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id> <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id> <quarkus.platform.version>3.0.0.Final</quarkus.platform.version> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <antlr4.version>4.10.1</antlr4.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>${quarkus.platform.group-id}</groupId> <artifactId>${quarkus.platform.artifact-id}</artifactId> <version>${quarkus.platform.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- Quarkus dependencies --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive</artifactId> </dependency> <!-- ANTLR runtime --> <dependency> <groupId>org.antlr</groupId> <artifactId>antlr4-runtime</artifactId> <version>${antlr4.version}</version> </dependency> </dependencies> <build> <plugins> <!-- ANTLR plugin to generate parser code --> <plugin> <groupId>org.antlr</groupId> <artifactId>antlr4-maven-plugin</artifactId> <version>${antlr4.version}</version> <executions> <execution> <id>generate-antlr</id> <phase>generate-sources</phase> <goals> <goal>antlr4</goal> </goals> <configuration> <sourceDirectory>src/main/antlr4</sourceDirectory> <outputDirectory>${project.build.directory}/generated-sources/antlr4</outputDirectory> </configuration> </execution> </executions> </plugin> <!-- Quarkus Maven plugin --> <plugin> <groupId>io.quarkus</groupId> <artifactId>quarkus-maven-plugin</artifactId> <version>${quarkus.platform.version}</version> <executions> <execution> <goals> <goal>build</goal> <goal>generate-code</goal> </goals> </execution> </executions> </plugin> <!-- Compiler plugin to include generated sources --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.10.1</version> <configuration> <source>${maven.compiler.source}</source> <target>${maven.compiler.target}</target> <generatedSourcesDirectory>${project.build.directory}/generated-sources/antlr4</generatedSourcesDirectory> </configuration> </plugin> </plugins> </build> </project>

Important note: In Quarkus dev mode, sometimes the ANTLR plugin doesn't run automatically unless you explicitly run mvn generate-sources before quarkus:dev. Lightrun


ExpressionEvaluator.java

package com.example; import org.antlr.v4.runtime.*; import org.antlr.v4.runtime.tree.*; public class ExpressionEvaluator { public int evaluate(String input) { CharStream cs = CharStreams.fromString(input); ExprLexer lexer = new ExprLexer(cs); CommonTokenStream tokens = new CommonTokenStream(lexer); ExprParser parser = new ExprParser(tokens); ParseTree tree = parser.prog(); EvalVisitor visitor = new EvalVisitor(); return visitor.visit(tree); } private static class EvalVisitor extends ExprBaseVisitor<Integer> { @Override public Integer visitMulDiv(ExprParser.MulDivContext ctx) { int left = visit(ctx.expr(0)); int right = visit(ctx.expr(1)); if (ctx.op.getText().equals("*")) { return left * right; } else { return left / right; } } @Override public Integer visitAddSub(ExprParser.AddSubContext ctx) { int left = visit(ctx.expr(0)); int right = visit(ctx.expr(1)); if (ctx.op.getText().equals("+")) { return left + right; } else { return left - right; } } @Override public Integer visitInt(ExprParser.IntContext ctx) { return Integer.parseInt(ctx.INT().getText()); } @Override public Integer visitParens(ExprParser.ParensContext ctx) { return visit(ctx.expr()); } } }

ExpressionResource.java

package com.example; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @Path("/expr") public class ExpressionResource { ExpressionEvaluator evaluator = new ExpressionEvaluator(); @GET @Produces(MediaType.TEXT_PLAIN) public Response eval(@QueryParam("input") String input) { try { int result = evaluator.evaluate(input); return Response.ok(String.valueOf(result)).build(); } catch (Exception e) { return Response.status(Response.Status.BAD_REQUEST) .entity("Error: " + e.getMessage()) .build(); } } }

With this endpoint, you can call:

GET /expr?input=2*(3+4)

and get back 14.


Testing & Running

  • Run in dev mode:

    mvn clean generate-sources quarkus:dev
  • In production:

    mvn package java -jar target/quarkus-antlr-example-1.0.0-SNAPSHOT-runner.jar
  • Test via HTTP call, e.g.:

    curl "http://localhost:8080/expr?input=5+6*2"

Additional Tips & Caveats

  • ANTLR version mismatches can lead to runtime errors (ATN deserialization issues) when mixing versions. Make sure the version used to generate the parser matches the runtime version. GitHub+1

  • For native image builds, dynamic reflection used by ANTLR may need explicit registration or substitutions. Quarkus extensions sometimes provide built-in support.

  • If your grammar grows complex, consider organizing it modularly (lexer grammar, parser grammar, etc.).

  • Use custom error listeners to provide better syntax error messages.

  • For large grammars, incremental parsing or partial parsing might be needed, depending on performance.

ANTLR or others ?

 

What is ANTLR?

ANTLR (ANother Tool for Language Recognition) is a powerful parser generator used to read, process, execute, or translate structured text or binary input. It’s widely used for building compilers, interpreters, DSLs, and expression parsers.


✅ Best Alternatives to ANTLR (Depending on Use Case)

ToolBest ForLanguage SupportNotes
ANTLRGeneral-purpose grammar parsingJava, C#, Python, JavaScript, Go, Swift, C++Powerful, with great grammar language and tooling
PEG.js / peggyLightweight JavaScript DSL parsersJavaScript, TypeScriptSimple PEG-based parsers for browsers and Node.js
JavaCCJava-based compiler constructionJavaMature but lower-level than ANTLR
Bison / YaccTraditional compiler constructionC / C++Used in system-level parsers, old but robust
ANTLR4 + QuarkusReactive microservices with expression/DSL parsingJava (Quarkus)Combine ANTLR and Quarkus for expression parsing in APIs
ParboiledParsing in Java/Kotlin/Scala without grammar filesJava, Kotlin, ScalaCode-based, elegant, but less expressive than ANTLR
OhmEducation & DSL designJavaScriptVisual + grammar editing; great for prototyping
ChevrotainPerformance-critical parsing in JSJavaScript, TypeScriptFastest JavaScript parser combinator toolkit
RagelFinite state machines + protocolsC, C++, JavaGreat for network protocols and binary formats
Tree-sitterIncremental parsing for editorsRust / CUsed in editors (VSCode, Neovim, Atom) for syntax parsing

🥇 Best Overall: ANTLR (for Most Use Cases)

  • 🛠️ Ideal for DSLs, expressions, configuration files, etc.

  • 💬 Excellent grammar language (EBNF-style)

  • 🔄 Generates parse trees and ASTs automatically

  • 🔌 Works across many languages (Java, Python, JS, etc.)

  • 👨‍🏫 Huge community, learning resources, and examples

  • 🔧 Great tooling: plugin support for IntelliJ, VSCode, Maven, Gradle

Use ANTLR if:

  • You want full control over grammar and syntax

  • You’re building interpreters or compilers

  • You want a battle-tested parser generator


🧠 If You Want a Code-Based Approach (Not Grammar Files)

Use Parboiled (Java/Kotlin) or Chevrotain (JS)

  • These let you build parsers in code, without writing grammar files

  • Easier to debug and integrate in modern applications

  • Great for embedding parsers in microservices (e.g., Quarkus/Vert.x)


⚡ If You Need Editor-Friendly or Live Parsing

Use Tree-sitter or Ohm

  • Useful for syntax highlighting, IDE features, or interactive language tools

  • Tree-sitter supports incremental parsing for live code editing

  • Ohm offers visual tools for learning and debugging grammars


🧪 Bonus: Combine ANTLR with Quarkus

If you’re building a Quarkus app and want to parse:

  • Math expressions

  • Query DSLs

  • Config languages

  • Custom APIs

👉 Use ANTLR to define your language and integrate it into a Quarkus REST or Kafka microservice for parsing or validating inputs.


🔚 Conclusion: Which is Best?

Use CaseBest Option
General DSLs / Expression ParsingANTLR
Java-only compiler toolsJavaCC
Interactive grammars / prototypingOhm
Performance in JSChevrotain
Editor tooling (VSCode/Neovim)Tree-sitter
Parsing in Java without grammar filesParboiled
Protocols / FSMsRagel

Using Schema Registry in Quarkus app

Using Schema Registry in Quarkus Application Development

Schema Registry is a critical component when working with Apache Kafka and Avro in Quarkus applications. It ensures that the data structure (schema) is consistent and compatible across producers and consumers. Here's a concise guide to integrating Schema Registry in a Quarkus application using a Student object.


1. Add Dependencies

Include the necessary dependencies in your pom.xml for Kafka, Avro, and Schema Registry support:

<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-apicurio-registry-avro</artifactId> </dependency>

2. Configure Application Properties

Set up the connection to your Schema Registry in the application.properties file:

# Kafka broker configuration kafka.bootstrap.servers=localhost:9092 # Schema Registry configuration mp.messaging.connector.smallrye-kafka.schema.registry.url=http://localhost:8081 # Avro serialization mp.messaging.outgoing.my-topic.value.serializer=io.apicurio.registry.utils.serde.AvroKafkaSerializer mp.messaging.incoming.my-topic.value.deserializer=io.apicurio.registry.utils.serde.AvroKafkaDeserializer

🔁 Replace http://localhost:8081 with the URL of your Schema Registry (e.g., Confluent or Apicurio).


3. Define Avro Schema

Create Avro schema files (e.g., student.avsc) and generate Java classes using the Avro Maven plugin:

Example: student.avsc

{ "namespace": "com.example.avro", "type": "record", "name": "Student", "fields": [ { "name": "name", "type": "string" }, { "name": "email", "type": "string" }, { "name": "grade", "type": "int" } ] }

Add the Avro Maven Plugin to pom.xml:

<plugin> <groupId>org.apache.avro</groupId> <artifactId>avro-maven-plugin</artifactId> <version>1.11.1</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>schema</goal> </goals> <configuration> <sourceDirectory>${project.basedir}/src/main/avro</sourceDirectory> <outputDirectory>${project.build.directory}/generated-sources/avro</outputDirectory> </configuration> </execution> </executions> </plugin>

Place your student.avsc file in src/main/avro.


4. Implement Kafka Producers and Consumers

Use Quarkus' reactive messaging to produce and consume messages with the Student Avro object.

✅ Producer Example:

import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.reactive.messaging.Channel; import org.eclipse.microprofile.reactive.messaging.Emitter; import com.example.avro.Student; @ApplicationScoped public class KafkaStudentProducer { @Inject @Channel("my-topic") Emitter<Student> emitter; public void send(Student student) { emitter.send(student); } }

✅ Consumer Example:

import org.eclipse.microprofile.reactive.messaging.Incoming; import com.example.avro.Student; public class KafkaStudentConsumer { @Incoming("my-topic") public void consume(Student student) { System.out.println("Received student: " + student); } }

Here, Student is the Java class generated from your Avro schema (student.avsc).


5. Test Schema Compatibility

Ensure your producer and consumer schemas are compatible. The Schema Registry (e.g., Apicurio or Confluent) will validate compatibility automatically at runtime.


✅ Summary

By following these steps, you can seamlessly integrate Schema Registry into your Quarkus application, enabling robust, versioned, and schema-compliant Kafka messaging with Avro.

Using the Student object example, you’ve seen:

  • How to configure Schema Registry

  • Define and generate Avro classes

  • Produce and consume Kafka messages with reactive messaging

  • Automatically validate schema compatibility at runtime


💡 Tip: If you're using Apicurio Registry, you can also explore features like artifact versioning, API-based registration, and schema evolution rules (BACKWARD, FORWARD, FULL compatibility).

Avro ↔ JSON Conversion in Java

 

Full Maven Project Template

In this guide, you’ll learn how to:

  • Define an Avro schema for a Product (fields: name, code, price)

  • Read Avro-serialized data (generic or specific)

  • Convert Avro records to JSON via Jackson

  • Use a full Maven project setup to tie it all together


1. Project Structure

Here’s a suggested directory layout:

product-avro-json/ ├── pom.xml ├── src │ ├── main │ │ ├── avro │ │ │ └── product.avsc │ │ └── java │ │ └── com/example │ │ ├── AvroReader.java │ │ ├── AvroToJsonConverter.java │ │ └── MainApp.java │ └── test │ └── java │ └── com/example │ └── AvroJsonTest.java

2. pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>product-avro-json</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <avro.version>1.10.2</avro.version> <jackson.version>2.12.3</jackson.version> </properties> <dependencies> <!-- Avro runtime --> <dependency> <groupId>org.apache.avro</groupId> <artifactId>avro</artifactId> <version>${avro.version}</version> </dependency> <!-- Avro compiler (for code generation) --> <dependency> <groupId>org.apache.avro</groupId> <artifactId>avro-compiler</artifactId> <version>${avro.version}</version> </dependency> <!-- Jackson for JSON conversion --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <!-- JUnit (for testing) --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.7.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.7.0</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- Avro Maven Plugin: generate Java classes from .avsc --> <plugin> <groupId>org.apache.avro</groupId> <artifactId>avro-maven-plugin</artifactId> <version>${avro.version}</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>schema</goal> </goals> <configuration> <sourceDirectory>${project.basedir}/src/main/avro</sourceDirectory> <outputDirectory>${project.build.directory}/generated-sources/avro</outputDirectory> </configuration> </execution> </executions> </plugin> <!-- Ensure generated sources are compiled --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>11</source> <target>11</target> <annotationProcessorPaths> <!-- include avro plugin if needed --> </annotationProcessorPaths> </configuration> <executions> <execution> <id>default-compile</id> <goals><goal>compile</goal></goals> </execution> <execution> <id>default-testCompile</id> <goals><goal>testCompile</goal></goals> </execution> </executions> </plugin> </plugins> </build> </project>

This pom.xml does:

  • Include avro and avro-compiler

  • Use the avro-maven-plugin to generate Java classes from .avsc

  • Include Jackson for JSON serialization

  • Setup compilation and test dependencies


3. Avro Schema: product.avsc

Place this in src/main/avro/product.avsc:

{ "namespace": "com.example.avro", "type": "record", "name": "Product", "fields": [ { "name": "name", "type": "string" }, { "name": "code", "type": "string" }, { "name": "price", "type": "double" } ] }

After you build the project, the Avro plugin will produce a generated Java class com.example.avro.Product.


4. Java Source Files

AvroReader.java

package com.example; import org.apache.avro.file.DataFileReader; import org.apache.avro.generic.GenericDatumReader; import org.apache.avro.generic.GenericRecord; import org.apache.avro.generic.GenericDatumReader; import org.apache.avro.io.DatumReader; import java.io.File; import java.io.IOException; public class AvroReader { public static Iterable<GenericRecord> readGenericRecords(String avroFilePath) throws IOException { File file = new File(avroFilePath); DatumReader<GenericRecord> datumReader = new GenericDatumReader<>(); DataFileReader<GenericRecord> dataFileReader = new DataFileReader<>(file, datumReader); return dataFileReader; } public static <T> Iterable<T> readSpecificRecords(String avroFilePath, Class<T> clazz) throws IOException { // Not full implementation; for specific, you’d use SpecificDatumReader throw new UnsupportedOperationException("Not implemented"); } }

AvroToJsonConverter.java

package com.example; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.avro.generic.GenericRecord; import java.io.IOException; public class AvroToJsonConverter { private static final ObjectMapper objectMapper = new ObjectMapper(); public static String convertGenericRecordToJson(GenericRecord record) throws IOException { return objectMapper.writeValueAsString(record); } }

MainApp.java

package com.example; import org.apache.avro.generic.GenericRecord; import java.io.IOException; public class MainApp { public static void main(String[] args) { if (args.length != 1) { System.err.println("Usage: java -jar product-avro-json.jar <path-to-avro-file>"); System.exit(1); } String avroFilePath = args[0]; try { for (GenericRecord rec : AvroReader.readGenericRecords(avroFilePath)) { String json = AvroToJsonConverter.convertGenericRecordToJson(rec); System.out.println(json); } } catch (IOException e) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(); } } }

5. (Optional) Test Example: AvroJsonTest.java

You could put a test to check the conversion:

package com.example; import org.apache.avro.generic.GenericRecord; import org.junit.jupiter.api.Test; import java.io.File; import java.io.IOException; import static org.junit.jupiter.api.Assertions.*; public class AvroJsonTest { @Test public void testConversion() throws IOException { String avroFile = "src/test/resources/test-products.avro"; Iterable<GenericRecord> recs = AvroReader.readGenericRecords(avroFile); boolean found = false; for (GenericRecord rec : recs) { String json = AvroToJsonConverter.convertGenericRecordToJson(rec); assertTrue(json.contains("\"name\"")); assertTrue(json.contains("\"code\"")); assertTrue(json.contains("\"price\"")); found = true; } assertTrue(found, "No record found in test Avro file"); } }

You’d have to supply a test Avro file in src/test/resources/test-products.avro.


6. How to Build & Run

  1. Compile & generate sources:

    mvn clean compile

    The Avro plugin will generate Java classes from product.avsc in the target/generated-sources/avro directory.

  2. Package into a JAR:

    mvn package

    This produces product-avro-json-1.0-SNAPSHOT.jar in target/.

  3. Run the application:

    java -jar target/product-avro-json-1.0-SNAPSHOT.jar path/to/your/products.avro

    It will print JSON lines corresponding to each record in the Avro file.


7. Explanation & Notes

  • The Avro Maven plugin reads .avsc files in src/main/avro and generates Java classes under target/generated-sources/avro.

  • In this example, we use generic records via GenericRecord to read data; this is flexible and schema-driven.

  • We use Jackson to convert Avro GenericRecord to JSON.

  • If you prefer specific records (i.e. use the generated Product class directly), you can replace generic reading with SpecificDatumReader<Product> and then do objectMapper.writeValueAsString(productInstance).

  • Be careful about null values or optional fields if you extend the schema.

ANTLR 4 into a Quarkus app

  Using ANTLR 4 with Quarkus: A Practical Guide Why combine ANTLR + Quarkus? Quarkus gives you a fast, cloud/native Java framework with ...