How to Add Validations to the ANTLR Grammar
When building a custom query language or a flexible filter system, ANTLR is one of the most powerful tools you can use. It allows you to…
How to Add Validations to the ANTLR Grammar
When building a custom query language or a flexible filter system, ANTLR is one of the most powerful tools you can use. It allows you to define a grammar and automatically generate parsers, making it much easier to handle complex expressions.
In a previous article, I showed how we can use ANTLR grammar to create a filter for a REST API. That worked well, but the first version was missing an important capability: validating field types. Without validation, users could try to compare incompatible types (e.g., comparing a string to a number) or even query fields that don’t exist.
In this article, we’ll walk through how to extend the grammar with type validation, ensuring that filters are not only syntactically correct but also semantically valid.

Generated by ChatGPT.
Recap
The grammar used for a simple filter looks like this:
- An expression can be:
expression AND/OR expression - An expression is composed of:
field operator value fieldis any stringvaluecan be: string, boolean, number, ornull- A
numbercan be an integer or a double (integer + '.' + integer)
grammar Filter;
expression: expression AND expression
| expression OR expression
| field operator value;
operator: 'eq' | 'ne' | 'gt' | 'ge' | 'lt' | 'le';
value: STRING | number | BOOLEAN | NULL;
number: INTEGER | INTEGER '.' INTEGER;
field: FIELD_NAME;
AND: 'and';
OR: 'or';
NULL: 'null';
BOOLEAN: 'true' | 'false';
STRING: '\'' (~['\\] | '\\' .)* '\'';
INTEGER: [0-9]+;
FIELD_NAME: [a-z] [a-z0-9_-]*;
WS: [ \t\r\n]+ -> skip;
The code to interpret the grammar is as follows:
public class QueryFilterListener extends FilterBaseListener {
private Integer fieldCounter = 1;
private Stack<Object> stack;
private final Stack<FilterResponse> filterResponseStack = new Stack<>();
public FilterResponse getResult() {
return filterResponseStack.pop();
}
@Override
public void enterField(FilterParser.FieldContext ctx) {
stack.push(ctx.getText());
}
@Override
public void enterValue(FilterParser.ValueContext ctx) {
stack.push(
ctx.STRING() != null
? ctx.STRING().getText().substring(1, ctx.STRING().getText().length() - 1)
: ctx.getText());
}
@Override
public void enterOperator(FilterParser.OperatorContext ctx) {
switch (ctx.getText()) {
case "eq" -> stack.push("=");
case "lt" -> stack.push("<");
case "gt" -> stack.push(">");
case "le" -> stack.push("<=");
case "ge" -> stack.push(">=");
case "ne" -> stack.push("<>");
default -> throw new IllegalArgumentException("Unexpected value: " + ctx.getText());
}
}
@Override
public void enterExpression(FilterParser.ExpressionContext ctx) {
stack = new Stack<>();
}
@Override
public void exitExpression(FilterParser.ExpressionContext ctx) {
if (ctx.AND() != null || ctx.OR() != null) {
var right = filterResponseStack.pop();
var left = filterResponseStack.pop();
Map<Integer, Object> mergedMap = new HashMap<>(left.parameters());
mergedMap.putAll(right.parameters());
filterResponseStack.push(
new FilterResponse(
left.where() + (ctx.AND() != null ? " and " : " or ") + right.where(), mergedMap));
} else {
var value = (String) stack.pop();
var operator = (String) stack.pop();
var field = (String) stack.pop();
String where = String.format("%s %s %s", field, operator, "?" + fieldCounter);
Map<Integer, Object> parameters = Map.of(fieldCounter, value);
filterResponseStack.push(new FilterResponse(where, parameters));
fieldCounter++;
}
}
}
Testing the Grammar
Now, let’s add some tests to check our grammar.
When running them, you’ll notice the tests fail because:
- It does not check the type of the field being compared.
- It does not check if the field actually exists.
@Test
void errorWhenComparingIdWithString() {
Assertions.assertThrows(
IllegalArgumentException.class,
() -> productRepository.findByFilter("id eq '1'"));
}
@Test
void errorWhenComparingNameWithInteger() {
Assertions.assertThrows(
IllegalArgumentException.class,
() -> productRepository.findByFilter("name eq 1"));
}
@Test
void errorWhenUnknownField() {
Assertions.assertThrows(
IllegalArgumentException.class,
() -> productRepository.findByFilter("unknownField eq 1"));
}
The Solution
To fix these issues, we extend the grammar interpreter with a new QueryFilterListener class:
public class QueryFilterListener extends FilterBaseListener {
private static final Map<String, Class<?>> TYPE_MAP =
Map.of(
"id",
Long.class,
"price",
Number.class,
"name",
String.class,
"description",
String.class,
"category",
String.class);
private Integer fieldCounter = 1;
private Stack<Object> stack;
private final Stack<FilterResponse> filterResponseStack = new Stack<>();
public FilterResponse getResult() {
return filterResponseStack.pop();
}
@Override
public void enterField(FilterParser.FieldContext ctx) {
stack.push(new Field(ctx.getText(), TYPE_MAP.getOrDefault(ctx.getText(), String.class)));
}
@Override
public void enterValue(FilterParser.ValueContext ctx) {
Class<?> valueType = determineValueType(ctx);
var field = (Field) stack.get(stack.size() - 2);
if (valueType != null
&& field.type() != valueType
&& !field.type().isAssignableFrom(valueType)) {
throw new IllegalArgumentException(
"Field type mismatch: expected " + field.type() + ", found " + valueType);
}
stack.push(
ctx.STRING() != null
? ctx.STRING().getText().substring(1, ctx.STRING().getText().length() - 1)
: ctx.getText());
}
private Class<?> determineValueType(FilterParser.ValueContext ctx) {
if (ctx.STRING() != null) {
return String.class;
} else if (ctx.BOOLEAN() != null) {
return Boolean.class;
} else if (ctx.number() != null) {
return ctx.number().INTEGER().size() == 1 ? Integer.class : Double.class;
} else {
return null;
}
}
@Override
public void enterOperator(FilterParser.OperatorContext ctx) {
switch (ctx.getText()) {
case "eq" -> stack.push("=");
case "lt" -> stack.push("<");
case "gt" -> stack.push(">");
case "le" -> stack.push("<=");
case "ge" -> stack.push(">=");
case "ne" -> stack.push("<>");
default -> throw new IllegalArgumentException("Unexpected value: " + ctx.getText());
}
}
@Override
public void enterExpression(FilterParser.ExpressionContext ctx) {
stack = new Stack<>();
}
@Override
public void exitExpression(FilterParser.ExpressionContext ctx) {
if (ctx.AND() != null || ctx.OR() != null) {
var right = filterResponseStack.pop();
var left = filterResponseStack.pop();
Map<Integer, Object> mergedMap = new HashMap<>(left.parameters());
mergedMap.putAll(right.parameters());
filterResponseStack.push(
new FilterResponse(
left.where() + (ctx.AND() != null ? " and " : " or ") + right.where(), mergedMap));
} else {
var value = (String) stack.pop();
var operator = (String) stack.pop();
var field = (Field) stack.pop();
String where = String.format("%s %s %s", field.name(), operator, "?" + fieldCounter);
Map<Integer, Object> parameters = Map.of(fieldCounter, value);
filterResponseStack.push(new FilterResponse(where, parameters));
fieldCounter++;
}
}
private record Field(String name, Class<?> type) {}
}
What Changed?
- A static
Mapdefines the field names and their types. - A new
Fieldrecord was created to store both the field name and its type. - On
enterField, we push aFieldrecord onto the stack. - A new method
determineValueTypewas added, which returns the type of the input value. - On
enterValue, a simple validation checks if the field type matches the provided value.
Conclusion
By adding type validation to our ANTLR grammar, we made our filter parser much more robust. Now, it doesn’t just parse expressions — it ensures they actually make sense according to the fields and types we support.
This approach can be extended further:
- Enforcing stricter operators depending on types (e.g.,
<only for numbers). - Validating nested fields or object structures.
- Providing meaningful error messages to the end user.
If you’re building a domain-specific language (DSL) or any query/filter system, investing time in validations will save you from subtle bugs and confusing user errors.
ANTLR gives us the power to go beyond syntax — and start enforcing semantics. 🚀
메타데이터
- post_id
- 0f5e7ecf5452
- slug
- how-to-add-validations-to-the-antlr-grammar-0f5e7ecf5452
- url
- https://medium.com/@renanschmitt/how-to-add-validations-to-the-antlr-grammar-0f5e7ecf5452
- canonical_url
- https://medium.com/@renanschmitt/how-to-add-validations-to-the-antlr-grammar-0f5e7ecf5452
- author_url
- https://medium.com/@renanschmitt
- status
- ok
- fetched_at
- 2026-06-24 11:06:28