diff --git a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java
index 37cf2cfd3c1..1f5cda8aa4e 100644
--- a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java
+++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java
@@ -28,10 +28,6 @@ import org.assertj.core.api.AbstractStringAssert;
import org.assertj.core.api.AssertProvider;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures;
-import org.skyscreamer.jsonassert.JSONCompare;
-import org.skyscreamer.jsonassert.JSONCompareMode;
-import org.skyscreamer.jsonassert.JSONCompareResult;
-import org.skyscreamer.jsonassert.comparator.JSONComparator;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
@@ -41,7 +37,6 @@ import org.springframework.core.io.Resource;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
-import org.springframework.util.function.ThrowingBiFunction;
/**
* Base AssertJ {@linkplain org.assertj.core.api.Assert assertions} that can be
@@ -51,8 +46,8 @@ import org.springframework.util.function.ThrowingBiFunction;
* extracting a part of the document for further {@linkplain JsonPathValueAssert
* assertions} on the value.
*
- *
Also supports comparing the JSON document against a target, using
- * {@linkplain JSONCompare JSON Assert}. Resources that are loaded from
+ *
Also supports comparing the JSON document against a target, using a
+ * {@linkplain JsonComparator JSON Comparator}. Resources that are loaded from
* the classpath can be relative if a {@linkplain #withResourceLoadClass(Class)
* class} is provided. By default, {@code UTF-8} is used to load resources,
* but this can be overridden using {@link #withCharset(Charset)}.
@@ -154,9 +149,9 @@ public abstract class AbstractJsonContentAssertThe resource abstraction allows to provide several input types:
*
@@ -231,11 +226,11 @@ public abstract class AbstractJsonContentAssertThe resource abstraction allows to provide several input types:
*
@@ -259,7 +254,7 @@ public abstract class AbstractJsonContentAssertThe resource abstraction allows to provide several input types:
*
@@ -347,11 +342,11 @@ public abstract class AbstractJsonContentAssertThe resource abstraction allows to provide several input types:
*
@@ -375,7 +370,7 @@ public abstract class AbstractJsonContentAssert
- JSONCompare.compareJSON(expectedJsonString, actualJsonString, compareMode));
+ private JsonComparison compare(@Nullable CharSequence expectedJson, JsonCompareMode compareMode) {
+ return compare(expectedJson, JsonAssert.comparator(compareMode));
}
- private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONComparator comparator) {
- return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) ->
- JSONCompare.compareJSON(expectedJsonString, actualJsonString, comparator));
+ private JsonComparison compare(@Nullable CharSequence expectedJson, JsonComparator comparator) {
+ return comparator.compare((expectedJson != null) ? expectedJson.toString() : null, this.actual);
}
- private JSONCompareResult compare(@Nullable CharSequence actualJson, @Nullable CharSequence expectedJson,
- ThrowingBiFunction comparator) {
-
- if (actualJson == null) {
- return compareForNull(expectedJson);
- }
- if (expectedJson == null) {
- return compareForNull(actualJson.toString());
- }
- try {
- return comparator.applyWithException(actualJson.toString(), expectedJson.toString());
- }
- catch (Exception ex) {
- if (ex instanceof RuntimeException runtimeException) {
- throw runtimeException;
- }
- throw new IllegalStateException(ex);
- }
+ private SELF assertIsMatch(JsonComparison result) {
+ return assertComparison(result, JsonComparison.Result.MATCH);
}
- private JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) {
- JSONCompareResult result = new JSONCompareResult();
- if (expectedJson != null) {
- result.fail("Expected null JSON");
- }
- return result;
- }
-
- private SELF assertNotFailed(JSONCompareResult result) {
- if (result.failed()) {
- failWithMessage("JSON comparison failure: %s", result.getMessage());
- }
- return this.myself;
+ private SELF assertIsMismatch(JsonComparison result) {
+ return assertComparison(result, JsonComparison.Result.MISMATCH);
}
- private SELF assertNotPassed(JSONCompareResult result) {
- if (result.passed()) {
- failWithMessage("JSON comparison failure: %s", result.getMessage());
+ private SELF assertComparison(JsonComparison jsonComparison, JsonComparison.Result requiredResult) {
+ if (jsonComparison.getResult() != requiredResult) {
+ failWithMessage("JSON comparison failure: %s", jsonComparison.getMessage());
}
return this.myself;
}
diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonAssert.java
new file mode 100644
index 00000000000..377c85f2051
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/json/JsonAssert.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.json;
+
+import org.skyscreamer.jsonassert.JSONCompare;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+import org.skyscreamer.jsonassert.JSONCompareResult;
+import org.skyscreamer.jsonassert.comparator.JSONComparator;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.function.ThrowingBiFunction;
+
+/**
+ * Useful methods that can be used with {@code org.skyscreamer.jsonassert}.
+ *
+ * @author Phillip Webb
+ * @since 6.2
+ */
+public abstract class JsonAssert {
+
+ /**
+ * Create a {@link JsonComparator} from the given {@link JsonCompareMode}.
+ * @param compareMode the mode to use
+ * @return a new {@link JsonComparator} instance
+ * @see JSONCompareMode#STRICT
+ * @see JSONCompareMode#LENIENT
+ */
+ public static JsonComparator comparator(JsonCompareMode compareMode) {
+ return comparator(toJSONCompareMode(compareMode));
+ }
+
+ /**
+ * Create a new {@link JsonComparator} from the given JSONAssert
+ * {@link JSONComparator}.
+ * @param comparator the JSON Assert {@link JSONComparator}
+ * @return a new {@link JsonComparator} instance
+ */
+ public static JsonComparator comparator(JSONComparator comparator) {
+ return comparator((expectedJson, actualJson) -> JSONCompare
+ .compareJSON(expectedJson, actualJson, comparator));
+ }
+
+ /**
+ * Create a new {@link JsonComparator} from the given JSONAssert
+ * {@link JSONCompareMode}.
+ * @param mode the JSON Assert {@link JSONCompareMode}
+ * @return a new {@link JsonComparator} instance
+ */
+ public static JsonComparator comparator(JSONCompareMode mode) {
+ return comparator((expectedJson, actualJson) -> JSONCompare
+ .compareJSON(expectedJson, actualJson, mode));
+ }
+
+ private static JsonComparator comparator(ThrowingBiFunction compareFunction) {
+ return (expectedJson, actualJson) -> compare(expectedJson, actualJson, compareFunction);
+ }
+
+ private static JsonComparison compare(@Nullable String expectedJson, @Nullable String actualJson,
+ ThrowingBiFunction compareFunction) {
+
+ if (actualJson == null) {
+ return (expectedJson != null)
+ ? JsonComparison.mismatch("Expected null JSON")
+ : JsonComparison.match();
+ }
+ if (expectedJson == null) {
+ return JsonComparison.mismatch("Expected non-null JSON");
+ }
+ JSONCompareResult result = compareFunction.throwing(IllegalStateException::new).apply(expectedJson, actualJson);
+ return (!result.passed())
+ ? JsonComparison.mismatch(result.getMessage())
+ : JsonComparison.match();
+ }
+
+ private static JSONCompareMode toJSONCompareMode(JsonCompareMode compareMode) {
+ return (compareMode != JsonCompareMode.LENIENT ? JSONCompareMode.STRICT : JSONCompareMode.LENIENT);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonComparator.java b/spring-test/src/main/java/org/springframework/test/json/JsonComparator.java
new file mode 100644
index 00000000000..bc8dae2ee8e
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/json/JsonComparator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.json;
+
+import org.springframework.lang.Nullable;
+import org.springframework.test.json.JsonComparison.Result;
+
+/**
+ * Strategy interface used to compare JSON strings.
+ *
+ * @author Phillip Webb
+ * @since 6.2
+ * @see JsonAssert
+ */
+@FunctionalInterface
+public interface JsonComparator {
+
+ /**
+ * Compare the given JSON strings.
+ * @param expectedJson the expected JSON
+ * @param actualJson the actual JSON
+ * @return the JSON comparison
+ */
+ JsonComparison compare(@Nullable String expectedJson, @Nullable String actualJson);
+
+ /**
+ * Assert that the {@code expectedJson} matches the comparison rules of ths
+ * instance against the {@code actualJson}. Throw an {@link AssertionError}
+ * if the comparison does not match.
+ * @param expectedJson the expected JSON
+ * @param actualJson the actual JSON
+ */
+ default void assertIsMatch(@Nullable String expectedJson, @Nullable String actualJson) {
+ JsonComparison comparison = compare(expectedJson, actualJson);
+ if (comparison.getResult() == Result.MISMATCH) {
+ throw new AssertionError(comparison.getMessage());
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonCompareMode.java b/spring-test/src/main/java/org/springframework/test/json/JsonCompareMode.java
new file mode 100644
index 00000000000..b82be904c85
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/json/JsonCompareMode.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.json;
+
+/**
+ * Modes that can be used to compare JSON.
+ *
+ * @author Phillip Webb
+ * @since 6.2
+ */
+public enum JsonCompareMode {
+
+ /**
+ * Strict checking.
+ */
+ STRICT,
+
+ /**
+ * Lenient checking.
+ */
+ LENIENT
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonComparison.java b/spring-test/src/main/java/org/springframework/test/json/JsonComparison.java
new file mode 100644
index 00000000000..6930bc98333
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/json/JsonComparison.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.json;
+
+import org.springframework.lang.Nullable;
+
+/**
+ * A comparison of two JSON strings as returned from a {@link JsonComparator}.
+ *
+ * @author Phillip Webb
+ * @since 6.2
+ */
+public final class JsonComparison {
+
+ private final Result result;
+
+ @Nullable
+ private final String message;
+
+
+ private JsonComparison(Result result, @Nullable String message) {
+ this.result = result;
+ this.message = message;
+ }
+
+ /**
+ * Factory method to create a new {@link JsonComparison} when the JSON
+ * strings match.
+ * @return a new {@link JsonComparison} instance
+ */
+ public static JsonComparison match() {
+ return new JsonComparison(Result.MATCH, null);
+ }
+
+ /**
+ * Factory method to create a new {@link JsonComparison} when the JSON strings
+ * do not match.
+ * @param message a message describing the mismatch
+ * @return a new {@link JsonComparison} instance
+ */
+ public static JsonComparison mismatch(String message) {
+ return new JsonComparison(Result.MISMATCH, message);
+ }
+
+ /**
+ * Return the result of the comparison.
+ */
+ public Result getResult() {
+ return this.result;
+ }
+
+ /**
+ * Return a message describing the comparison.
+ */
+ @Nullable
+ public String getMessage() {
+ return this.message;
+ }
+
+
+ /**
+ * Comparison results.
+ */
+ public enum Result {
+
+ /**
+ * The JSON strings match when considering the comparison rules.
+ */
+ MATCH,
+
+ /**
+ * The JSON strings do not match when considering the comparison rules.
+ */
+ MISMATCH
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/util/JsonExpectationsHelper.java b/spring-test/src/main/java/org/springframework/test/util/JsonExpectationsHelper.java
index 7c6489a6da0..a6b29e70ea5 100644
--- a/spring-test/src/main/java/org/springframework/test/util/JsonExpectationsHelper.java
+++ b/spring-test/src/main/java/org/springframework/test/util/JsonExpectationsHelper.java
@@ -18,6 +18,8 @@ package org.springframework.test.util;
import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.test.json.JsonComparator;
+
/**
* A helper class for assertions on JSON content.
*
@@ -26,7 +28,10 @@ import org.skyscreamer.jsonassert.JSONAssert;
*
* @author Sebastien Deleuze
* @since 4.1
+ * @deprecated in favor of using {@link JSONAssert} directly or the
+ * {@link JsonComparator} abstraction
*/
+@Deprecated(since = "6.2")
public class JsonExpectationsHelper {
/**
diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java
index faf19a13142..36678088637 100644
--- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java
+++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java
@@ -42,7 +42,10 @@ import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.mock.http.MockHttpInputMessage;
import org.springframework.mock.http.client.MockClientHttpRequest;
-import org.springframework.test.util.JsonExpectationsHelper;
+import org.springframework.test.json.JsonAssert;
+import org.springframework.test.json.JsonComparator;
+import org.springframework.test.json.JsonCompareMode;
+import org.springframework.test.json.JsonComparison;
import org.springframework.test.util.XmlExpectationsHelper;
import org.springframework.test.web.client.RequestMatcher;
import org.springframework.util.LinkedMultiValueMap;
@@ -71,8 +74,6 @@ public class ContentRequestMatchers {
private final XmlExpectationsHelper xmlHelper;
- private final JsonExpectationsHelper jsonHelper;
-
/**
* Class constructor, not for direct instantiation.
@@ -80,7 +81,6 @@ public class ContentRequestMatchers {
*/
protected ContentRequestMatchers() {
this.xmlHelper = new XmlExpectationsHelper();
- this.jsonHelper = new JsonExpectationsHelper();
}
@@ -331,7 +331,7 @@ public class ContentRequestMatchers {
* @since 5.0.5
*/
public RequestMatcher json(String expectedJsonContent) {
- return json(expectedJsonContent, false);
+ return json(expectedJsonContent, JsonCompareMode.LENIENT);
}
/**
@@ -348,12 +348,43 @@ public class ContentRequestMatchers {
* @param expectedJsonContent the expected JSON content
* @param strict enables strict checking
* @since 5.0.5
+ * @deprecated in favor of {@link #json(String, JsonCompareMode)}
*/
+ @Deprecated(since = "6.2")
public RequestMatcher json(String expectedJsonContent, boolean strict) {
+ JsonCompareMode compareMode = (strict ? JsonCompareMode.STRICT : JsonCompareMode.LENIENT);
+ return json(expectedJsonContent, compareMode);
+ }
+
+ /**
+ * Parse the request body and the given string as JSON and assert the two
+ * using the given {@linkplain JsonCompareMode mode}. If the comparison failed,
+ * throws an {@link AssertionError} with the message of the {@link JsonComparison}.
+ * Use of this matcher requires the JSONassert library.
+ * @param expectedJsonContent the expected JSON content
+ * @param compareMode the compare mode
+ * @since 6.2
+ */
+ public RequestMatcher json(String expectedJsonContent, JsonCompareMode compareMode) {
+ return json(expectedJsonContent, JsonAssert.comparator(compareMode));
+ }
+
+ /**
+ * Parse the request body and the given string as JSON and assert the two
+ * using the given {@link JsonComparator}. If the comparison failed, throws an
+ * {@link AssertionError} with the message of the {@link JsonComparison}.
+ *
Use this matcher if you require a custom JSONAssert configuration or
+ * if you desire to use another assertion library.
+ * @param expectedJsonContent the expected JSON content
+ * @param comparator the comparator to use
+ * @since 6.2
+ */
+ public RequestMatcher json(String expectedJsonContent, JsonComparator comparator) {
return request -> {
try {
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
- this.jsonHelper.assertJsonEqual(expectedJsonContent, mockRequest.getBodyAsString(), strict);
+ comparator.assertIsMatch(expectedJsonContent, mockRequest.getBodyAsString());
}
catch (Exception ex) {
throw new AssertionError("Failed to parse expected or actual JSON request content", ex);
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
index da784ae0faa..4a57df70440 100644
--- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
@@ -45,9 +45,11 @@ import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.lang.Nullable;
+import org.springframework.test.json.JsonAssert;
+import org.springframework.test.json.JsonComparator;
+import org.springframework.test.json.JsonCompareMode;
import org.springframework.test.util.AssertionErrors;
import org.springframework.test.util.ExceptionCollector;
-import org.springframework.test.util.JsonExpectationsHelper;
import org.springframework.test.util.XmlExpectationsHelper;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -658,10 +660,22 @@ class DefaultWebTestClient implements WebTestClient {
}
@Override
+ @Deprecated(since = "6.2")
public BodyContentSpec json(String json, boolean strict) {
+ JsonCompareMode compareMode = (strict ? JsonCompareMode.STRICT : JsonCompareMode.LENIENT);
+ return json(json, compareMode);
+ }
+
+ @Override
+ public BodyContentSpec json(String expectedJson, JsonCompareMode compareMode) {
+ return json(expectedJson, JsonAssert.comparator(compareMode));
+ }
+
+ @Override
+ public BodyContentSpec json(String expectedJson, JsonComparator comparator) {
this.result.assertWithDiagnostics(() -> {
try {
- new JsonExpectationsHelper().assertJsonEqual(json, getBodyAsString(), strict);
+ comparator.assertIsMatch(expectedJson, getBodyAsString());
}
catch (Exception ex) {
throw new AssertionError("JSON parsing error", ex);
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
index 96938ac7b79..9b94b74f66f 100644
--- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
@@ -40,6 +40,9 @@ import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.lang.Nullable;
+import org.springframework.test.json.JsonComparator;
+import org.springframework.test.json.JsonCompareMode;
+import org.springframework.test.json.JsonComparison;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
@@ -997,10 +1000,10 @@ public interface WebTestClient {
* JSONassert library
* to be on the classpath.
* @param expectedJson the expected JSON content
- * @see #json(String, boolean)
+ * @see #json(String, JsonCompareMode)
*/
default BodyContentSpec json(String expectedJson) {
- return json(expectedJson, false);
+ return json(expectedJson, JsonCompareMode.LENIENT);
}
/**
@@ -1019,9 +1022,37 @@ public interface WebTestClient {
* @param strict enables strict checking if {@code true}
* @since 5.3.16
* @see #json(String)
+ * @deprecated in favor of {@link #json(String, JsonCompareMode)}
*/
+ @Deprecated(since = "6.2")
BodyContentSpec json(String expectedJson, boolean strict);
+ /**
+ * Parse the expected and actual response content as JSON and perform a
+ * comparison using the given {@linkplain JsonCompareMode mode}. If the
+ * comparison failed, throws an {@link AssertionError} with the message
+ * of the {@link JsonComparison}.
+ *
Use of this method requires the
+ * JSONassert library
+ * to be on the classpath.
+ * @param expectedJson the expected JSON content
+ * @param compareMode the compare mode
+ * @since 6.2
+ * @see #json(String)
+ */
+ BodyContentSpec json(String expectedJson, JsonCompareMode compareMode);
+
+ /**
+ * Parse the expected and actual response content as JSON and perform a
+ * comparison using the given {@link JsonComparator}. If the comparison
+ * failed, throws an {@link AssertionError} with the message of the
+ * {@link JsonComparison}.
+ * @param expectedJson the expected JSON content
+ * @param comparator the comparator to use
+ * @since 6.2
+ */
+ BodyContentSpec json(String expectedJson, JsonComparator comparator);
+
/**
* Parse expected and actual response content as XML and assert that
* the two are "similar", i.e. they contain the same elements and
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java
index dcd08ba6de2..1dc4cf89848 100644
--- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java
@@ -28,7 +28,10 @@ import org.hamcrest.Matcher;
import org.w3c.dom.Node;
import org.springframework.http.MediaType;
-import org.springframework.test.util.JsonExpectationsHelper;
+import org.springframework.test.json.JsonAssert;
+import org.springframework.test.json.JsonComparator;
+import org.springframework.test.json.JsonCompareMode;
+import org.springframework.test.json.JsonComparison;
import org.springframework.test.util.XmlExpectationsHelper;
import org.springframework.test.web.servlet.ResultMatcher;
@@ -51,8 +54,6 @@ public class ContentResultMatchers {
private final XmlExpectationsHelper xmlHelper;
- private final JsonExpectationsHelper jsonHelper;
-
/**
* Protected constructor.
@@ -60,7 +61,6 @@ public class ContentResultMatchers {
*/
protected ContentResultMatchers() {
this.xmlHelper = new XmlExpectationsHelper();
- this.jsonHelper = new JsonExpectationsHelper();
}
@@ -198,13 +198,16 @@ public class ContentResultMatchers {
/**
* Parse the expected and actual strings as JSON and assert the two
* are "similar" - i.e. they contain the same attribute-value pairs
- * regardless of formatting with a lenient checking (extensible, and non-strict array
- * ordering).
+ * regardless of formatting with a lenient checking (extensible,
+ * and non-strict array ordering).
+ *
Use of this matcher requires the JSONassert library.
* @param jsonContent the expected JSON content
* @since 4.1
+ * @see #json(String, JsonCompareMode)
*/
public ResultMatcher json(String jsonContent) {
- return json(jsonContent, false);
+ return json(jsonContent, JsonCompareMode.LENIENT);
}
/**
@@ -220,11 +223,42 @@ public class ContentResultMatchers {
* @param jsonContent the expected JSON content
* @param strict enables strict checking
* @since 4.2
+ * @deprecated in favor of {@link #json(String, JsonCompareMode)}
*/
+ @Deprecated(since = "6.2")
public ResultMatcher json(String jsonContent, boolean strict) {
+ JsonCompareMode compareMode = (strict ? JsonCompareMode.STRICT : JsonCompareMode.LENIENT);
+ return json(jsonContent, compareMode);
+ }
+
+ /**
+ * Parse the response content and the given string as JSON and assert the two
+ * using the given {@linkplain JsonCompareMode mode}. If the comparison failed,
+ * throws an {@link AssertionError} with the message of the {@link JsonComparison}.
+ *
Use of this matcher requires the JSONassert library.
+ * @param jsonContent the expected JSON content
+ * @param compareMode the compare mode
+ * @since 6.2
+ */
+ public ResultMatcher json(String jsonContent, JsonCompareMode compareMode) {
+ return json(jsonContent, JsonAssert.comparator(compareMode));
+ }
+
+ /**
+ * Parse the response content and the given string as JSON and assert the two
+ * using the given {@link JsonComparator}. If the comparison failed, throws an
+ * {@link AssertionError} with the message of the {@link JsonComparison}.
+ *
Use this matcher if you require a custom JSONAssert configuration or
+ * if you desire to use another assertion library.
+ * @param jsonContent the expected JSON content
+ * @param comparator the comparator to use
+ * @since 6.2
+ */
+ public ResultMatcher json(String jsonContent, JsonComparator comparator) {
return result -> {
String content = result.getResponse().getContentAsString(StandardCharsets.UTF_8);
- this.jsonHelper.assertJsonEqual(jsonContent, content, strict);
+ comparator.assertIsMatch(jsonContent, content);
};
}
diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/ContentResultMatchersDsl.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/ContentResultMatchersDsl.kt
index 2ef41fdf1a0..f8322e40ddf 100644
--- a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/ContentResultMatchersDsl.kt
+++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/ContentResultMatchersDsl.kt
@@ -18,6 +18,7 @@ package org.springframework.test.web.servlet.result
import org.hamcrest.Matcher
import org.springframework.http.MediaType
+import org.springframework.test.json.JsonCompareMode
import org.springframework.test.web.servlet.ResultActions
import org.w3c.dom.Node
import javax.xml.transform.Source
@@ -112,7 +113,16 @@ class ContentResultMatchersDsl internal constructor (private val actions: Result
/**
* @see ContentResultMatchers.json
*/
- fun json(jsonContent: String, strict: Boolean = false) {
- actions.andExpect(matchers.json(jsonContent, strict))
+ @Deprecated(message = "Use JsonCompare mode instead")
+ fun json(jsonContent: String, strict: Boolean) {
+ val compareMode = (if (strict) JsonCompareMode.STRICT else JsonCompareMode.LENIENT)
+ actions.andExpect(matchers.json(jsonContent, compareMode))
+ }
+
+ /**
+ * @see ContentResultMatchers.json
+ */
+ fun json(jsonContent: String, compareMode: JsonCompareMode = JsonCompareMode.LENIENT) {
+ actions.andExpect(matchers.json(jsonContent, compareMode))
}
}
diff --git a/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java
index 14f1ad8dcf2..37568a6d585 100644
--- a/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java
+++ b/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java
@@ -28,6 +28,8 @@ import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.AssertProvider;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
@@ -37,7 +39,7 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.skyscreamer.jsonassert.JSONCompareMode;
-import org.skyscreamer.jsonassert.comparator.DefaultComparator;
+import org.skyscreamer.jsonassert.JSONCompareResult;
import org.skyscreamer.jsonassert.comparator.JSONComparator;
import org.springframework.core.ParameterizedTypeReference;
@@ -55,6 +57,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.entry;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
/**
* Tests for {@link AbstractJsonContentAssert}.
@@ -79,7 +85,7 @@ class AbstractJsonContentAssertTests {
private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter =
new MappingJackson2HttpMessageConverter(new ObjectMapper());
- private static final JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);
+ private static final JsonComparator comparator = JsonAssert.comparator(JsonCompareMode.LENIENT);
@Test
void isNullWhenActualIsNullShouldPass() {
@@ -364,29 +370,29 @@ class AbstractJsonContentAssertTests {
void isEqualToWhenExpectedIsNullShouldFail() {
CharSequence actual = null;
assertThatExceptionOfType(AssertionError.class)
- .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(actual, JSONCompareMode.LENIENT));
+ .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(actual, JsonCompareMode.LENIENT));
}
@Test
void isEqualToWhenStringIsMatchingAndLenientShouldPass() {
- assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT);
+ assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, JsonCompareMode.LENIENT);
}
@Test
void isEqualToWhenStringIsNotMatchingAndLenientShouldFail() {
assertThatExceptionOfType(AssertionError.class)
- .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, JSONCompareMode.LENIENT));
+ .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, JsonCompareMode.LENIENT));
}
@Test
void isEqualToWhenResourcePathIsMatchingAndLenientShouldPass() {
- assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", JSONCompareMode.LENIENT);
+ assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", JsonCompareMode.LENIENT);
}
@Test
void isEqualToWhenResourcePathIsNotMatchingAndLenientShouldFail() {
assertThatExceptionOfType(AssertionError.class)
- .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", JSONCompareMode.LENIENT));
+ .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", JsonCompareMode.LENIENT));
}
Stream source() {
@@ -416,14 +422,14 @@ class AbstractJsonContentAssertTests {
@ParameterizedTest
@MethodSource("lenientSame")
void isEqualToWhenResourceIsMatchingAndLenientSameShouldPass(Resource expected) {
- assertThat(forJson(SOURCE)).isEqualTo(expected, JSONCompareMode.LENIENT);
+ assertThat(forJson(SOURCE)).isEqualTo(expected, JsonCompareMode.LENIENT);
}
@ParameterizedTest
@MethodSource("different")
void isEqualToWhenResourceIsNotMatchingAndLenientShouldFail(Resource expected) {
assertThatExceptionOfType(AssertionError.class).isThrownBy(
- () -> assertThat(forJson(SOURCE)).isEqualTo(expected, JSONCompareMode.LENIENT));
+ () -> assertThat(forJson(SOURCE)).isEqualTo(expected, JsonCompareMode.LENIENT));
}
@Test
@@ -568,36 +574,36 @@ class AbstractJsonContentAssertTests {
@Test
void isNotEqualToWhenStringIsMatchingAndLenientShouldFail() {
assertThatExceptionOfType(AssertionError.class)
- .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT));
+ .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, JsonCompareMode.LENIENT));
}
@Test
void isNotEqualToWhenStringIsNotMatchingAndLenientShouldPass() {
- assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, JSONCompareMode.LENIENT);
+ assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, JsonCompareMode.LENIENT);
}
@Test
void isNotEqualToWhenResourcePathIsMatchingAndLenientShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(
- () -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", JSONCompareMode.LENIENT));
+ () -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", JsonCompareMode.LENIENT));
}
@Test
void isNotEqualToWhenResourcePathIsNotMatchingAndLenientShouldPass() {
- assertThat(forJson(SOURCE)).isNotEqualTo("different.json", JSONCompareMode.LENIENT);
+ assertThat(forJson(SOURCE)).isNotEqualTo("different.json", JsonCompareMode.LENIENT);
}
@ParameterizedTest
@MethodSource("lenientSame")
void isNotEqualToWhenResourceIsMatchingAndLenientShouldFail(Resource expected) {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(forJson(SOURCE))
- .isNotEqualTo(expected, JSONCompareMode.LENIENT));
+ .isNotEqualTo(expected, JsonCompareMode.LENIENT));
}
@ParameterizedTest
@MethodSource("different")
void isNotEqualToWhenResourceIsNotMatchingAndLenientShouldPass(Resource expected) {
- assertThat(forJson(SOURCE)).isNotEqualTo(expected, JSONCompareMode.LENIENT);
+ assertThat(forJson(SOURCE)).isNotEqualTo(expected, JsonCompareMode.LENIENT);
}
@Test
@@ -715,6 +721,28 @@ class AbstractJsonContentAssertTests {
assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected);
}
+ @Test
+ void isEqualToWithCustomCompareMode() {
+ String differentOrder = """
+ {
+ "spring": [
+ "framework",
+ "boot"
+ ]
+ }
+ """;
+ assertThat(forJson(SOURCE)).isEqualTo(differentOrder, JsonAssert.comparator(JSONCompareMode.NON_EXTENSIBLE));
+ }
+
+ @Test
+ void isEqualToWithCustomJsonComparator() throws JSONException {
+ String empty = "{}";
+ JSONComparator comparator = mock(JSONComparator.class);
+ given(comparator.compareJSON(any(JSONObject.class), any(JSONObject.class))).willReturn(new JSONCompareResult());
+ assertThat(forJson(SOURCE)).isEqualTo(empty, JsonAssert.comparator(comparator));
+ verify(comparator).compareJSON(any(JSONObject.class), any(JSONObject.class));
+ }
+
@Test
void withResourceLoadClassShouldAllowToLoadRelativeContent() {
AbstractJsonContentAssert> jsonAssert = assertThat(forJson(NULLS)).withResourceLoadClass(String.class);
@@ -730,6 +758,31 @@ class AbstractJsonContentAssertTests {
}
}
+ @Nested
+ class JsonComparatorTests {
+
+ private final JsonComparator comparator = mock(JsonComparator.class);
+
+ @Test
+ void isEqualToInvokesComparator() {
+ given(comparator.compare("{ }", "{}")).willReturn(JsonComparison.match());
+ assertThat(forJson("{}")).isEqualTo("{ }", this.comparator);
+ verify(comparator).compare("{ }", "{}");
+ }
+
+ @Test
+ void isEqualToWithNoMatchProvidesErrorMessage() {
+ given(comparator.compare("{ }", "{}")).willReturn(JsonComparison.mismatch("No additional whitespace expected"));
+ assertThatExceptionOfType(AssertionError.class)
+ .isThrownBy(() -> assertThat(forJson("{}")).isEqualTo("{ }", this.comparator))
+ .withMessageContaining("No additional whitespace expected");
+ verify(comparator).compare("{ }", "{}");
+ }
+
+ private AssertProvider> forJson(@Nullable String json) {
+ return () -> new TestJsonContentAssert(json, null).withResourceLoadClass(getClass());
+ }
+ }
private Consumer hasFailedToMatchPath(String expression) {
return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java
index 31d8efd3693..ffd33380ffe 100644
--- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java
+++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java
@@ -23,6 +23,7 @@ import reactor.core.publisher.Flux;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
+import org.springframework.test.json.JsonCompareMode;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -74,7 +75,7 @@ class JsonContentTests {
{"firstName":"John", "lastName":"Smith"}
]
""",
- true);
+ JsonCompareMode.STRICT);
}
@Test
@@ -89,7 +90,7 @@ class JsonContentTests {
{"firstName":"John"}
]
""",
- true)
+ JsonCompareMode.STRICT)
);
}
diff --git a/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt b/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt
index eea4afe9cf9..ff03bee71c8 100644
--- a/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt
+++ b/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt
@@ -27,6 +27,7 @@ import org.springframework.http.MediaType.APPLICATION_ATOM_XML
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.http.MediaType.APPLICATION_XML
import org.springframework.http.MediaType.TEXT_PLAIN
+import org.springframework.test.json.JsonCompareMode
import org.springframework.test.web.Person
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.bind.annotation.GetMapping
@@ -64,7 +65,7 @@ class MockMvcExtensionsTests {
status { isOk() }
content { contentType(APPLICATION_JSON) }
jsonPath("$.name") { value("Lee") }
- content { json("""{"someBoolean": false}""", false) }
+ content { json("""{"someBoolean": false}""", JsonCompareMode.LENIENT) }
}.andDo {
print()
}
@@ -130,7 +131,7 @@ class MockMvcExtensionsTests {
status { isOk() }
content { contentType(APPLICATION_JSON) }
jsonPath("$.name") { value("Lee") }
- content { json("""{"someBoolean": false}""", false) }
+ content { json("""{"someBoolean": false}""", JsonCompareMode.LENIENT) }
}.andDo {
print()
}