diff --git a/src/main/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluator.java b/src/main/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluator.java
index 82dc216..1a59724 100644
--- a/src/main/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluator.java
+++ b/src/main/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluator.java
@@ -48,6 +48,15 @@
* org.opentest4j.AssertionFailedError} with expected and actual fields preserved via {@link
* FailureCollector#rewrap(String, AssertionError)}.
*
+ *
The {@code path} field on the assertion follows the same {@code response.*} convention as
+ * {@link JsonSchemaAssertionEvaluator}, resolved via {@link ResponseValueExtractor}:
+ *
+ *
+ * - {@code response.body.json} — compare the entire parsed JSON body
+ *
- {@code response.body.json.} — compare only the value at the given JSONPath
+ * expression
+ *
+ *
* Current limitation: only top-level field names are supported in the ignore list. Nested
* JSONPath expressions are not yet evaluated.
*/
@@ -110,8 +119,22 @@ public void evaluate(ApiResponse response, FailureCollector collector) {
return;
}
+ JsonNode actualNode;
+ switch (ResponseValueExtractor.extract(response, assertion.path())) {
+ case ResponseValueExtractor.Result.Found found -> actualNode = objectMapper.valueToTree(found.value());
+ case ResponseValueExtractor.Result.Missing missing -> {
+ collector.fail(
+ String.format("Path '%s' not found in response for json_match assertion", missing.path()),
+ null);
+ return;
+ }
+ case ResponseValueExtractor.Result.Error error -> {
+ collector.fail(error.message(), null);
+ return;
+ }
+ }
+
try {
- JsonNode actualNode = objectMapper.valueToTree(response.body().json());
JsonNode expectedNode = objectMapper.readTree(expectedJson);
removeIgnoredFields(actualNode, assertion.expected().ignore());
diff --git a/src/test/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluatorTest.java b/src/test/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluatorTest.java
index fa8d014..9da9cca 100644
--- a/src/test/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluatorTest.java
+++ b/src/test/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluatorTest.java
@@ -45,8 +45,22 @@ private static JsonMatchAssertionEvaluator evaluator(String expectedContent, Lis
private static JsonMatchAssertionEvaluator evaluator(
String expectedContent, List ignore, Map suiteVars, Map testVars) {
+ return evaluatorForPath("response.body.json", expectedContent, ignore, suiteVars, testVars);
+ }
+
+ private static JsonMatchAssertionEvaluator evaluatorForPath(
+ String path, String expectedContent, List ignore) {
+ return evaluatorForPath(path, expectedContent, ignore, Map.of(), Map.of());
+ }
+
+ private static JsonMatchAssertionEvaluator evaluatorForPath(
+ String path,
+ String expectedContent,
+ List ignore,
+ Map suiteVars,
+ Map testVars) {
ObjectExpectedValue expected = new ObjectExpectedValue("inline", expectedContent, ignore);
- JsonMatchAssertion assertion = new JsonMatchAssertion("response.body.json", expected);
+ JsonMatchAssertion assertion = new JsonMatchAssertion(path, expected);
return new JsonMatchAssertionEvaluator(
assertion, null, OBJECT_MAPPER, Map.of("suite", suiteVars, "test", testVars));
}
@@ -201,4 +215,77 @@ void nullSuiteDirWithFileReferenceThrowsIllegalState() {
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Suite directory is required");
}
+
+ @Test
+ void jsonPathSubexpressionExtractsNestedObject() {
+ String text = "{\"data\":{\"id\":1}}";
+ Object json = Map.of("data", Map.of("id", 1));
+
+ FailureCollector collector = new FailureCollector();
+ evaluatorForPath("response.body.json.$.data", "{\"id\":1}", List.of())
+ .evaluate(responseWithJson(text, json), collector);
+
+ assertThatCode(collector::assertAll).doesNotThrowAnyException();
+ }
+
+ @Test
+ void jsonPathArrayElementExtractionMatchesIssueExample() {
+ String text = "[{\"data\":{\"id\":1}}]";
+ Object json = List.of(Map.of("data", Map.of("id", 1)));
+
+ FailureCollector collector = new FailureCollector();
+ evaluatorForPath("response.body.json.$[0].data", "{\"id\":1}", List.of())
+ .evaluate(responseWithJson(text, json), collector);
+
+ assertThatCode(collector::assertAll).doesNotThrowAnyException();
+ }
+
+ @Test
+ void jsonPathSubexpressionMismatchFails() {
+ String text = "{\"data\":{\"id\":1}}";
+ Object json = Map.of("data", Map.of("id", 1));
+
+ FailureCollector collector = new FailureCollector();
+ evaluatorForPath("response.body.json.$.data", "{\"id\":2}", List.of())
+ .evaluate(responseWithJson(text, json), collector);
+
+ assertThatThrownBy(collector::assertAll).isInstanceOf(MultipleFailuresError.class);
+ }
+
+ @Test
+ void missingJsonPathRecordsFailure() {
+ String text = "{\"data\":{\"id\":1}}";
+ Object json = Map.of("data", Map.of("id", 1));
+
+ FailureCollector collector = new FailureCollector();
+ evaluatorForPath("response.body.json.$.nonexistent", "{\"id\":1}", List.of())
+ .evaluate(responseWithJson(text, json), collector);
+
+ assertThatThrownBy(collector::assertAll).isInstanceOf(MultipleFailuresError.class);
+ }
+
+ @Test
+ void invalidJsonPathRecordsFailure() {
+ String text = "{\"data\":{\"id\":1}}";
+ Object json = Map.of("data", Map.of("id", 1));
+
+ FailureCollector collector = new FailureCollector();
+ evaluatorForPath("response.body.json.$[", "{\"id\":1}", List.of())
+ .evaluate(responseWithJson(text, json), collector);
+
+ assertThatThrownBy(collector::assertAll).isInstanceOf(MultipleFailuresError.class);
+ }
+
+ @Test
+ void unsupportedPathPrefixRecordsFailure() {
+ String text = "{\"data\":{\"id\":1}}";
+ Object json = Map.of("data", Map.of("id", 1));
+
+ FailureCollector collector = new FailureCollector();
+ evaluatorForPath("foo.bar", "{\"id\":1}", List.of()).evaluate(responseWithJson(text, json), collector);
+
+ assertThatThrownBy(collector::assertAll)
+ .isInstanceOf(MultipleFailuresError.class)
+ .hasMessageContaining("response.");
+ }
}