Problem
When a YAML field whose expected type is non-string (e.g. integer, boolean) is written with a Thymeleaf placeholder wrapped in double quotes:
assertions:
- type: status_code_equals
value: "[[${suite.expected_status}]]"
YAML parses the raw value as the string "[[${suite.expected_status}]]". After Thymeleaf substitutes 201, the YAML still contains:
Jackson deserialises "201" as String, not Integer, which causes type mismatch failures at assertion time. Without double quotes the YAML would correctly yield value: 201 after substitution.
Root cause: authors instinctively quote Thymeleaf placeholders in YAML to satisfy editors/linters, but those quotes survive into post-substitution parsing.
Fix: a pre-processing pass that strips double quotes from YAML values that are entirely a Thymeleaf placeholder — i.e. the entire quoted value starts with the template open-bracket ([[) and ends with the close-bracket (]]).
Behaviour
Given this input line:
value: "[[${suite.expected_status}]]"
The pre-processor outputs:
value: [[${suite.expected_status}]]
Given a mixed value (prefix or suffix outside the placeholder), the line is left unchanged:
url: "/api/[[${suite.version}]]/users" # NOT stripped — not purely a placeholder
Trailing YAML inline comments must be preserved:
value: "[[${suite.expected_status}]]" # expected HTTP status
becomes:
value: [[${suite.expected_status}]] # expected HTTP status
Configurable bracket markers
The open- and close-bracket tokens must be defined as named constants (not inlined in the regex) so that switching to a different templating engine later requires changing only those two values:
// Thymeleaf TEXT-mode inline syntax
private static final String PLACEHOLDER_OPEN = "[[";
private static final String PLACEHOLDER_CLOSE = "]]";
// Future: Pebble expression syntax
// private static final String PLACEHOLDER_OPEN = "{{";
// private static final String PLACEHOLDER_CLOSE = "}}";
The detection regex is built from these constants at class-initialisation time using Pattern.quote() so that bracket characters with regex meaning are escaped safely.
Implementation
New class: TemplatePreProcessor
Create src/main/java/.../service/TemplatePreProcessor.java (or util/ — place it next to FileLoader). It must be a Spring @Service (or a stateless utility) and thread-safe.
/**
* Pre-processes a raw YAML template string before Thymeleaf evaluation.
*
* <p>Strips double quotes from YAML scalar values that consist entirely of a
* Thymeleaf inline placeholder (a value whose content starts with {@value #PLACEHOLDER_OPEN}
* and ends with {@value #PLACEHOLDER_CLOSE}). This prevents YAML from locking a
* post-substitution integer or boolean into a string type.
*
* <p>The open- and close-bracket constants are intentionally named so they can be
* updated in one place if the project moves to a different template engine.
*
* <p>Thread-safety: stateless; the compiled {@link Pattern} is a static final field.
*/
public class TemplatePreProcessor {
private static final String PLACEHOLDER_OPEN = "[[";
private static final String PLACEHOLDER_CLOSE = "]]";
/** Matches: <indent><key><colon+space>"<open>...<close>"<optional trailing comment> */
private static final Pattern QUOTED_PLACEHOLDER = Pattern.compile(
"^(\\s*[\\w.-]+\\s*:\\s*)\"("
+ Pattern.quote(PLACEHOLDER_OPEN)
+ ".*?"
+ Pattern.quote(PLACEHOLDER_CLOSE)
+ ")\"(\\s*(?:#.*)?)$");
/**
* Processes {@code yaml} line by line and removes double quotes that wrap a
* pure Thymeleaf placeholder value.
*
* @param yaml raw YAML content (may be multi-line)
* @return the transformed YAML string; identical to {@code yaml} when no
* matching lines are found
*/
public String process(String yaml) { ... }
}
Algorithm inside process():
- Split the input on newlines (preserve the original line-ending style, or normalise to
\n and restore).
- For each line apply
QUOTED_PLACEHOLDER.matcher(line).
- If it matches, replace with
group(1) + group(2) + group(3) (key + unquoted placeholder + trailing comment/whitespace).
- If it does not match, keep the line unchanged.
- Rejoin and return.
Integration in TestSuiteLoader
In both load(Path) and load(Path, SuiteRunContext), call preProcessor.process(templateContent) immediately after Files.readString(filePath) and use the result everywhere downstream — including the value stored in TestSuite.templateContent (which PureJavaTestEngine uses for per-test re-parsing):
// load(Path filePath, SuiteRunContext context)
String rawYaml = Files.readString(filePath);
String templateContent = preProcessor.process(rawYaml); // ← new step
// ... existing Step 1 / Step 2 Thymeleaf passes use templateContent
Inject TemplatePreProcessor into TestSuiteLoader via constructor injection (GraalVM native image requires constructor injection).
Tests
TemplatePreProcessorTest (new):
| Scenario |
Expected outcome |
value: "[[${x}]]" |
value: [[${x}]] |
timeout: "[[${suite.timeout_ms}]]" |
timeout: [[${suite.timeout_ms}]] |
| Trailing comment preserved |
value: [[${x}]] # comment |
Mixed value "/api/[[${v}]]" |
unchanged |
No placeholder present value: "hello" |
unchanged |
| Multi-line input — only matching lines change |
all other lines preserved verbatim |
TestSuiteLoaderTest (update): add a test case where a suite YAML contains value: "[[${suite.my_int}]]", supplies my_int=42 as a suite variable, and asserts the assertion value field deserialises as Integer 42 (not String "42").
Implementation Notes
Pattern is compiled once as a static final field — thread-safe and efficient.
Pattern.quote() ensures bracket tokens with special regex meaning (e.g. future {{, }}) are escaped correctly without manual escaping.
- No runtime reflection — safe for GraalVM native image.
- Run
./mvnw spotless:apply before committing.
- All new classes and methods must have JavaDoc.
Problem
When a YAML field whose expected type is non-string (e.g.
integer,boolean) is written with a Thymeleaf placeholder wrapped in double quotes:YAML parses the raw value as the string
"[[${suite.expected_status}]]". After Thymeleaf substitutes201, the YAML still contains:Jackson deserialises
"201"asString, notInteger, which causes type mismatch failures at assertion time. Without double quotes the YAML would correctly yieldvalue: 201after substitution.Root cause: authors instinctively quote Thymeleaf placeholders in YAML to satisfy editors/linters, but those quotes survive into post-substitution parsing.
Fix: a pre-processing pass that strips double quotes from YAML values that are entirely a Thymeleaf placeholder — i.e. the entire quoted value starts with the template open-bracket (
[[) and ends with the close-bracket (]]).Behaviour
Given this input line:
The pre-processor outputs:
Given a mixed value (prefix or suffix outside the placeholder), the line is left unchanged:
Trailing YAML inline comments must be preserved:
becomes:
Configurable bracket markers
The open- and close-bracket tokens must be defined as named constants (not inlined in the regex) so that switching to a different templating engine later requires changing only those two values:
The detection regex is built from these constants at class-initialisation time using
Pattern.quote()so that bracket characters with regex meaning are escaped safely.Implementation
New class:
TemplatePreProcessorCreate
src/main/java/.../service/TemplatePreProcessor.java(orutil/— place it next toFileLoader). It must be a Spring@Service(or a stateless utility) and thread-safe.Algorithm inside
process():\nand restore).QUOTED_PLACEHOLDER.matcher(line).group(1) + group(2) + group(3)(key + unquoted placeholder + trailing comment/whitespace).Integration in
TestSuiteLoaderIn both
load(Path)andload(Path, SuiteRunContext), callpreProcessor.process(templateContent)immediately afterFiles.readString(filePath)and use the result everywhere downstream — including the value stored inTestSuite.templateContent(whichPureJavaTestEngineuses for per-test re-parsing):Inject
TemplatePreProcessorintoTestSuiteLoadervia constructor injection (GraalVM native image requires constructor injection).Tests
TemplatePreProcessorTest(new):value: "[[${x}]]"value: [[${x}]]timeout: "[[${suite.timeout_ms}]]"timeout: [[${suite.timeout_ms}]]value: [[${x}]] # comment"/api/[[${v}]]"value: "hello"TestSuiteLoaderTest(update): add a test case where a suite YAML containsvalue: "[[${suite.my_int}]]", suppliesmy_int=42as a suite variable, and asserts the assertionvaluefield deserialises asInteger 42(notString "42").Implementation Notes
Patternis compiled once as astatic finalfield — thread-safe and efficient.Pattern.quote()ensures bracket tokens with special regex meaning (e.g. future{{,}}) are escaped correctly without manual escaping../mvnw spotless:applybefore committing.