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 new file mode 100644 index 00000000000..45b7e839209 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java @@ -0,0 +1,501 @@ +/* + * 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 java.io.File; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.function.Consumer; + +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.PathNotFoundException; +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; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.InputStreamResource; +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 {@link org.assertj.core.api.Assert assertions} that can be + * applied to a JSON document. + * + *

Support evaluating {@linkplain JsonPath JSON path} expressions and + * extracting a part of the document for further {@linkplain JsonPathValueAssert + * assertions} on the value. + * + *

Also support comparing the JSON document against a target, using + * {@linkplain JSONCompare JSON Assert}. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @author Andy Wilkinson + * @author Diego Berrueta + * @author Camille Vienot + * @since 6.2 + * @param the type of assertions + */ +public abstract class AbstractJsonContentAssert> + extends AbstractStringAssert { + + private static final Failures failures = Failures.instance(); + + + @Nullable + private final GenericHttpMessageConverter jsonMessageConverter; + + private final JsonLoader jsonLoader; + + /** + * Create an assert for the given JSON document. + *

Path can be converted to a value object using the given + * {@linkplain GenericHttpMessageConverter json message converter}. + *

Resources to match can be loaded relative to the given + * {@code resourceLoadClass}. If not specified, resources must always be + * absolute. A specific {@link Charset} can be provided if {@code UTF-8} is + * not suitable. + * @param json the JSON document to assert + * @param jsonMessageConverter the converter to use + * @param resourceLoadClass the class used to load resources + * @param charset the charset of the JSON resources + * @param selfType the implementation type of this assert + */ + protected AbstractJsonContentAssert(@Nullable String json, + @Nullable GenericHttpMessageConverter jsonMessageConverter, @Nullable Class resourceLoadClass, + @Nullable Charset charset, Class selfType) { + super(json, selfType); + this.jsonMessageConverter = jsonMessageConverter; + this.jsonLoader = new JsonLoader(resourceLoadClass, charset); + as("JSON content"); + } + + // JsonPath support + + /** + * Verify that the given JSON {@code path} is present, and extract the JSON + * value for further {@linkplain JsonPathValueAssert assertions}. + * @param path the {@link JsonPath} expression + * @see #hasPathSatisfying(String, Consumer) + */ + public JsonPathValueAssert extractingPath(String path) { + Object value = new JsonPathValue(path).getValue(); + return new JsonPathValueAssert(value, path, this.jsonMessageConverter); + } + + /** + * Verify that the given JSON {@code path} is present with a JSON value + * satisfying the given {@code valueRequirements}. + * @param path the {@link JsonPath} expression + * @param valueRequirements a {@link Consumer} of the assertion object + */ + public SELF hasPathSatisfying(String path, Consumer> valueRequirements) { + Object value = new JsonPathValue(path).assertHasPath(); + JsonPathValueAssert valueAssert = new JsonPathValueAssert(value, path, this.jsonMessageConverter); + valueRequirements.accept(() -> valueAssert); + return this.myself; + } + + /** + * Verify that the given JSON {@code path} matches. For paths with an + * operator, this validates that the path expression is valid, but does not + * validate that it yield any results. + * @param path the {@link JsonPath} expression + */ + public SELF hasPath(String path) { + new JsonPathValue(path).assertHasPath(); + return this.myself; + } + + /** + * Verify that the given JSON {@code path} does not match. + * @param path the {@link JsonPath} expression + */ + public SELF doesNotHavePath(String path) { + new JsonPathValue(path).assertDoesNotHavePath(); + return this.myself; + } + + // JsonAssert support + + /** + * Verify that the actual value is equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param compareMode the compare mode used when checking + */ + public SELF isEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotFailed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + * @param compareMode the compare mode used when checking + */ + public SELF isEqualTo(Resource expected, JSONCompareMode compareMode) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotFailed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param comparator the comparator used when checking + */ + public SELF isEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotFailed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + * @param comparator the comparator used when checking + */ + public SELF isEqualTo(Resource expected, JSONComparator comparator) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotFailed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} + * equal to the given JSON. The {@code expected} value can contain the JSON + * itself or, if it ends with {@code .json}, the name of a resource to be + * loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public SELF isLenientlyEqualTo(@Nullable CharSequence expected) { + return isEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} + * equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + */ + public SELF isLenientlyEqualTo(Resource expected) { + return isEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#STRICT strictly} + * equal to the given JSON. The {@code expected} value can contain the JSON + * itself or, if it ends with {@code .json}, the name of a resource to be + * loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public SELF isStrictlyEqualTo(@Nullable CharSequence expected) { + return isEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#STRICT strictly} + * equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + */ + public SELF isStrictlyEqualTo(Resource expected) { + return isEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is not equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param compareMode the compare mode used when checking + */ + public SELF isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotPassed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is not equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + * @param compareMode the compare mode used when checking + */ + public SELF isNotEqualTo(Resource expected, JSONCompareMode compareMode) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotPassed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is not equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param comparator the comparator used when checking + */ + public SELF isNotEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotPassed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is not equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + * @param comparator the comparator used when checking + */ + public SELF isNotEqualTo(Resource expected, JSONComparator comparator) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotPassed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#LENIENT + * leniently} equal to the given JSON. The {@code expected} value can + * contain the JSON itself or, if it ends with {@code .json}, the name of a + * resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public SELF isNotLenientlyEqualTo(@Nullable CharSequence expected) { + return isNotEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#LENIENT + * leniently} equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + */ + public SELF isNotLenientlyEqualTo(Resource expected) { + return isNotEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#STRICT + * strictly} equal to the given JSON. The {@code expected} value can + * contain the JSON itself or, if it ends with {@code .json}, the name of a + * resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public SELF isNotStrictlyEqualTo(@Nullable CharSequence expected) { + return isNotEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#STRICT + * strictly} equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + */ + public SELF isNotStrictlyEqualTo(Resource expected) { + return isNotEqualTo(expected, JSONCompareMode.STRICT); + } + + + private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) { + return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> + JSONCompare.compareJSON(expectedJsonString, actualJsonString, compareMode)); + } + + private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONComparator comparator) { + return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> + JSONCompare.compareJSON(expectedJsonString, actualJsonString, comparator)); + } + + 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 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 assertNotPassed(JSONCompareResult result) { + if (result.passed()) { + failWithMessage("JSON comparison failure: %s", result.getMessage()); + } + return this.myself; + } + + private AssertionError failure(BasicErrorMessageFactory errorMessageFactory) { + throw failures.failure(this.info, errorMessageFactory); + } + + + /** + * A {@link JsonPath} value. + */ + private class JsonPathValue { + + private final String path; + + private final JsonPath jsonPath; + + private final String json; + + JsonPathValue(String path) { + Assert.hasText(path, "'path' must not be null or empty"); + isNotNull(); + this.path = path; + this.jsonPath = JsonPath.compile(this.path); + this.json = AbstractJsonContentAssert.this.actual; + } + + @Nullable + Object assertHasPath() { + return getValue(); + } + + void assertDoesNotHavePath() { + try { + read(); + throw failure(new JsonPathNotExpected(this.json, this.path)); + } + catch (PathNotFoundException ignore) { + } + } + + @Nullable + Object getValue() { + try { + return read(); + } + catch (PathNotFoundException ex) { + throw failure(new JsonPathNotFound(this.json, this.path)); + } + } + + @Nullable + private Object read() { + return this.jsonPath.read(this.json); + } + + + static final class JsonPathNotFound extends BasicErrorMessageFactory { + + private JsonPathNotFound(String actual, String path) { + super("%nExpecting:%n %s%nTo match JSON path:%n %s%n", actual, path); + } + } + + static final class JsonPathNotExpected extends BasicErrorMessageFactory { + + private JsonPathNotExpected(String actual, String path) { + super("%nExpecting:%n %s%nNot to match JSON path:%n %s%n", actual, path); + } + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java index a81c4724e1f..f36e7ddd7f5 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java @@ -55,7 +55,7 @@ public final class JsonContent implements AssertProvider { */ @Override public JsonContentAssert assertThat() { - return new JsonContentAssert(this.json, this.resourceLoadClass, null); + return new JsonContentAssert(this.json, null, this.resourceLoadClass, null); } /** diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java index 391c466c3fa..70534f7065f 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java @@ -16,351 +16,36 @@ package org.springframework.test.json; -import java.io.File; -import java.io.InputStream; import java.nio.charset.Charset; -import java.nio.file.Path; -import org.assertj.core.api.AbstractAssert; -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; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.InputStreamResource; -import org.springframework.core.io.Resource; +import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; -import org.springframework.util.function.ThrowingBiFunction; /** - * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied - * to a {@link CharSequence} representation of a JSON document, mostly to - * compare the JSON document against a target, using {@linkplain JSONCompare - * JSON Assert}. + * Default {@link AbstractJsonContentAssert} implementation. * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Diego Berrueta - * @author Camille Vienot * @author Stephane Nicoll * @since 6.2 */ -public class JsonContentAssert extends AbstractAssert { - - private final JsonLoader loader; +public class JsonContentAssert extends AbstractJsonContentAssert { /** - * Create a new {@link JsonContentAssert} instance that will load resources - * relative to the given {@code resourceLoadClass}, using the given - * {@code charset}. - * @param json the actual JSON content + * Create an assert for the given JSON document. + *

Path can be converted to a value object using the given + * {@linkplain GenericHttpMessageConverter json message converter}. + *

Resources to match can be loaded relative to the given + * {@code resourceLoadClass}. If not specified, resources must always be + * absolute. A specific {@link Charset} can be provided if {@code UTF-8} is + * not suitable. + * @param json the JSON document to assert + * @param jsonMessageConverter the converter to use * @param resourceLoadClass the class used to load resources * @param charset the charset of the JSON resources */ - public JsonContentAssert(@Nullable CharSequence json, @Nullable Class resourceLoadClass, - @Nullable Charset charset) { + public JsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter, + @Nullable Class resourceLoadClass, @Nullable Charset charset) { - super(json, JsonContentAssert.class); - this.loader = new JsonLoader(resourceLoadClass, charset); - } - - /** - * Create a new {@link JsonContentAssert} instance that will load resources - * relative to the given {@code resourceLoadClass}, using {@code UTF-8}. - * @param json the actual JSON content - * @param resourceLoadClass the class used to load resources - */ - public JsonContentAssert(@Nullable CharSequence json, @Nullable Class resourceLoadClass) { - this(json, resourceLoadClass, null); - } - - - /** - * Verify that the actual value is equal to the given JSON. The - * {@code expected} value can contain the JSON itself or, if it ends with - * {@code .json}, the name of a resource to be loaded from the classpath. - * @param expected the expected JSON or the name of a resource containing - * the expected JSON - * @param compareMode the compare mode used when checking - */ - public JsonContentAssert isEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { - String expectedJson = this.loader.getJson(expected); - return assertNotFailed(compare(expectedJson, compareMode)); - } - - /** - * Verify that the actual value is equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - * @param compareMode the compare mode used when checking - */ - public JsonContentAssert isEqualTo(Resource expected, JSONCompareMode compareMode) { - String expectedJson = this.loader.getJson(expected); - return assertNotFailed(compare(expectedJson, compareMode)); - } - - /** - * Verify that the actual value is equal to the given JSON. The - * {@code expected} value can contain the JSON itself or, if it ends with - * {@code .json}, the name of a resource to be loaded from the classpath. - * @param expected the expected JSON or the name of a resource containing - * the expected JSON - * @param comparator the comparator used when checking - */ - public JsonContentAssert isEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { - String expectedJson = this.loader.getJson(expected); - return assertNotFailed(compare(expectedJson, comparator)); - } - - /** - * Verify that the actual value is equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - * @param comparator the comparator used when checking - */ - public JsonContentAssert isEqualTo(Resource expected, JSONComparator comparator) { - String expectedJson = this.loader.getJson(expected); - return assertNotFailed(compare(expectedJson, comparator)); - } - - /** - * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} - * equal to the given JSON. The {@code expected} value can contain the JSON - * itself or, if it ends with {@code .json}, the name of a resource to be - * loaded from the classpath. - * @param expected the expected JSON or the name of a resource containing - * the expected JSON - */ - public JsonContentAssert isLenientlyEqualTo(@Nullable CharSequence expected) { - return isEqualTo(expected, JSONCompareMode.LENIENT); - } - - /** - * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} - * equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - */ - public JsonContentAssert isLenientlyEqualTo(Resource expected) { - return isEqualTo(expected, JSONCompareMode.LENIENT); - } - - /** - * Verify that the actual value is {@link JSONCompareMode#STRICT strictly} - * equal to the given JSON. The {@code expected} value can contain the JSON - * itself or, if it ends with {@code .json}, the name of a resource to be - * loaded from the classpath. - * @param expected the expected JSON or the name of a resource containing - * the expected JSON - */ - public JsonContentAssert isStrictlyEqualTo(@Nullable CharSequence expected) { - return isEqualTo(expected, JSONCompareMode.STRICT); - } - - /** - * Verify that the actual value is {@link JSONCompareMode#STRICT strictly} - * equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - */ - public JsonContentAssert isStrictlyEqualTo(Resource expected) { - return isEqualTo(expected, JSONCompareMode.STRICT); - } - - /** - * Verify that the actual value is not equal to the given JSON. The - * {@code expected} value can contain the JSON itself or, if it ends with - * {@code .json}, the name of a resource to be loaded from the classpath. - * @param expected the expected JSON or the name of a resource containing - * the expected JSON - * @param compareMode the compare mode used when checking - */ - public JsonContentAssert isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { - String expectedJson = this.loader.getJson(expected); - return assertNotPassed(compare(expectedJson, compareMode)); - } - - /** - * Verify that the actual value is not equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - * @param compareMode the compare mode used when checking - */ - public JsonContentAssert isNotEqualTo(Resource expected, JSONCompareMode compareMode) { - String expectedJson = this.loader.getJson(expected); - return assertNotPassed(compare(expectedJson, compareMode)); - } - - /** - * Verify that the actual value is not equal to the given JSON. The - * {@code expected} value can contain the JSON itself or, if it ends with - * {@code .json}, the name of a resource to be loaded from the classpath. - * @param expected the expected JSON or the name of a resource containing - * the expected JSON - * @param comparator the comparator used when checking - */ - public JsonContentAssert isNotEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { - String expectedJson = this.loader.getJson(expected); - return assertNotPassed(compare(expectedJson, comparator)); - } - - /** - * Verify that the actual value is not equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - * @param comparator the comparator used when checking - */ - public JsonContentAssert isNotEqualTo(Resource expected, JSONComparator comparator) { - String expectedJson = this.loader.getJson(expected); - return assertNotPassed(compare(expectedJson, comparator)); - } - - /** - * Verify that the actual value is not {@link JSONCompareMode#LENIENT - * leniently} equal to the given JSON. The {@code expected} value can - * contain the JSON itself or, if it ends with {@code .json}, the name of a - * resource to be loaded from the classpath. - * @param expected the expected JSON or the name of a resource containing - * the expected JSON - */ - public JsonContentAssert isNotLenientlyEqualTo(@Nullable CharSequence expected) { - return isNotEqualTo(expected, JSONCompareMode.LENIENT); - } - - /** - * Verify that the actual value is not {@link JSONCompareMode#LENIENT - * leniently} equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - */ - public JsonContentAssert isNotLenientlyEqualTo(Resource expected) { - return isNotEqualTo(expected, JSONCompareMode.LENIENT); - } - - /** - * Verify that the actual value is not {@link JSONCompareMode#STRICT - * strictly} equal to the given JSON. The {@code expected} value can - * contain the JSON itself or, if it ends with {@code .json}, the name of a - * resource to be loaded from the classpath. - * @param expected the expected JSON or the name of a resource containing - * the expected JSON - */ - public JsonContentAssert isNotStrictlyEqualTo(@Nullable CharSequence expected) { - return isNotEqualTo(expected, JSONCompareMode.STRICT); - } - - /** - * Verify that the actual value is not {@link JSONCompareMode#STRICT - * strictly} equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - */ - public JsonContentAssert isNotStrictlyEqualTo(Resource expected) { - return isNotEqualTo(expected, JSONCompareMode.STRICT); - } - - - private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) { - return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> - JSONCompare.compareJSON(expectedJsonString, actualJsonString, compareMode)); - } - - private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONComparator comparator) { - return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> - JSONCompare.compareJSON(expectedJsonString, actualJsonString, comparator)); - } - - 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 JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) { - JSONCompareResult result = new JSONCompareResult(); - if (expectedJson != null) { - result.fail("Expected null JSON"); - } - return result; - } - - private JsonContentAssert assertNotFailed(JSONCompareResult result) { - if (result.failed()) { - failWithMessage("JSON comparison failure: %s", result.getMessage()); - } - return this; - } - - private JsonContentAssert assertNotPassed(JSONCompareResult result) { - if (result.passed()) { - failWithMessage("JSON comparison failure: %s", result.getMessage()); - } - return this; + super(json, jsonMessageConverter, resourceLoadClass, charset, JsonContentAssert.class); } } diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java deleted file mode 100644 index 2ba28fcafd9..00000000000 --- a/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 java.util.function.Consumer; - -import com.jayway.jsonpath.JsonPath; -import com.jayway.jsonpath.PathNotFoundException; -import org.assertj.core.api.AbstractAssert; -import org.assertj.core.api.AssertProvider; -import org.assertj.core.error.BasicErrorMessageFactory; -import org.assertj.core.internal.Failures; - -import org.springframework.http.converter.GenericHttpMessageConverter; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied - * to a {@link CharSequence} representation of a JSON document using - * {@linkplain JsonPath JSON path}. - * - * @author Stephane Nicoll - * @since 6.2 - */ -public class JsonPathAssert extends AbstractAssert { - - private static final Failures failures = Failures.instance(); - - - @Nullable - private final GenericHttpMessageConverter jsonMessageConverter; - - - public JsonPathAssert(CharSequence json, - @Nullable GenericHttpMessageConverter jsonMessageConverter) { - - super(json, JsonPathAssert.class); - this.jsonMessageConverter = jsonMessageConverter; - } - - - /** - * Verify that the given JSON {@code path} is present, and extract the JSON - * value for further {@linkplain JsonPathValueAssert assertions}. - * @param path the {@link JsonPath} expression - * @see #hasPathSatisfying(String, Consumer) - */ - public JsonPathValueAssert extractingPath(String path) { - Object value = new JsonPathValue(path).getValue(); - return new JsonPathValueAssert(value, path, this.jsonMessageConverter); - } - - /** - * Verify that the given JSON {@code path} is present with a JSON value - * satisfying the given {@code valueRequirements}. - * @param path the {@link JsonPath} expression - * @param valueRequirements a {@link Consumer} of the assertion object - */ - public JsonPathAssert hasPathSatisfying(String path, Consumer> valueRequirements) { - Object value = new JsonPathValue(path).assertHasPath(); - JsonPathValueAssert valueAssert = new JsonPathValueAssert(value, path, this.jsonMessageConverter); - valueRequirements.accept(() -> valueAssert); - return this; - } - - /** - * Verify that the given JSON {@code path} matches. For paths with an - * operator, this validates that the path expression is valid, but does not - * validate that it yield any results. - * @param path the {@link JsonPath} expression - */ - public JsonPathAssert hasPath(String path) { - new JsonPathValue(path).assertHasPath(); - return this; - } - - /** - * Verify that the given JSON {@code path} does not match. - * @param path the {@link JsonPath} expression - */ - public JsonPathAssert doesNotHavePath(String path) { - new JsonPathValue(path).assertDoesNotHavePath(); - return this; - } - - - private AssertionError failure(BasicErrorMessageFactory errorMessageFactory) { - throw failures.failure(this.info, errorMessageFactory); - } - - - /** - * A {@link JsonPath} value. - */ - private class JsonPathValue { - - private final String path; - - private final JsonPath jsonPath; - - private final String json; - - JsonPathValue(String path) { - Assert.hasText(path, "'path' must not be null or empty"); - this.path = path; - this.jsonPath = JsonPath.compile(this.path); - this.json = JsonPathAssert.this.actual.toString(); - } - - @Nullable - Object assertHasPath() { - return getValue(); - } - - void assertDoesNotHavePath() { - try { - read(); - throw failure(new JsonPathNotExpected(this.json, this.path)); - } - catch (PathNotFoundException ignore) { - } - } - - @Nullable - Object getValue() { - try { - return read(); - } - catch (PathNotFoundException ex) { - throw failure(new JsonPathNotFound(this.json, this.path)); - } - } - - @Nullable - private Object read() { - return this.jsonPath.read(this.json); - } - - - static final class JsonPathNotFound extends BasicErrorMessageFactory { - - private JsonPathNotFound(String actual, String path) { - super("%nExpecting:%n %s%nTo match JSON path:%n %s%n", actual, path); - } - } - - static final class JsonPathNotExpected extends BasicErrorMessageFactory { - - private JsonPathNotExpected(String actual, String path) { - super("%nExpecting:%n %s%nNot to match JSON path:%n %s%n", actual, path); - } - } - } - -} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java index fffd4831502..202a495c4bb 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java @@ -25,8 +25,8 @@ import org.assertj.core.api.AbstractStringAssert; import org.springframework.core.io.ClassPathResource; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; +import org.springframework.test.json.AbstractJsonContentAssert; import org.springframework.test.json.JsonContentAssert; -import org.springframework.test.json.JsonPathAssert; /** * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to @@ -53,19 +53,20 @@ public class ResponseBodyAssert extends AbstractByteArrayAssert jsonPath() { + return new JsonContentAssert(getJson(), this.jsonMessageConverter, null, this.characterEncoding) + .as("JSON body"); } /** - * Return a new {@linkplain JsonContentAssert assertion} object that provides - * support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON - * assert} comparisons against expected JSON input which can be loaded from - * the classpath. + * Return a new {@linkplain AbstractJsonContentAssert assertion} object that + * provides support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode + * JSON assert} comparisons against expected JSON input which can be loaded + * from the classpath. *

This method only supports absolute locations for JSON documents loaded * from the classpath. Consider using {@link #json(Class)} to load JSON * documents relative to a given class. @@ -76,15 +77,15 @@ public class ResponseBodyAssert extends AbstractByteArrayAssert */ - public JsonContentAssert json() { + public AbstractJsonContentAssert json() { return json(null); } /** - * Return a new {@linkplain JsonContentAssert assertion} object that provides - * support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON - * assert} comparisons against expected JSON input which can be loaded from - * the classpath. + * Return a new {@linkplain AbstractJsonContentAssert assertion} object that + * provides support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode + * JSON assert} comparisons against expected JSON input which can be loaded + * from the classpath. *

Locations for JSON documents can be absolute using a leading slash, or * relative to the given {@code resourceLoadClass}. *

Example:


@@ -96,8 +97,9 @@ public class ResponseBodyAssert extends AbstractByteArrayAssert resourceLoadClass) {
-		return new JsonContentAssert(getJson(), resourceLoadClass, this.characterEncoding);
+	public AbstractJsonContentAssert json(@Nullable Class resourceLoadClass) {
+		return new JsonContentAssert(getJson(), this.jsonMessageConverter, resourceLoadClass, this.characterEncoding)
+				.as("JSON body");
 	}
 
 	/**
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
new file mode 100644
index 00000000000..5453aebcda9
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java
@@ -0,0 +1,781 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.assertj.core.api.AssertProvider;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.junit.jupiter.params.ParameterizedTest;
+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.comparator.JSONComparator;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.lang.Nullable;
+import org.springframework.util.FileCopyUtils;
+
+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;
+
+/**
+ * Tests for {@link AbstractJsonContentAssert}.
+ *
+ * @author Stephane Nicoll
+ * @author Phillip Webb
+ */
+class AbstractJsonContentAssertTests {
+
+	private static final String TYPES = loadJson("types.json");
+
+	private static final String SIMPSONS = loadJson("simpsons.json");
+
+	private static final String NULLS = loadJson("nulls.json");
+
+	private static final String SOURCE = loadJson("source.json");
+
+	private static final String LENIENT_SAME = loadJson("lenient-same.json");
+
+	private static final String DIFFERENT = loadJson("different.json");
+
+	private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter =
+			new MappingJackson2HttpMessageConverter(new ObjectMapper());
+
+	private static final JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);
+
+	@Test
+	void isNullWhenActualIsNullShouldPass() {
+		assertThat(forJson(null)).isNull();
+	}
+
+	@Nested
+	class HasPathTests {
+
+		@Test
+		void hasPathForNullJson() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(null)).hasPath("no"))
+					.withMessageContaining("Expecting actual not to be null");
+		}
+
+		@Test
+		void hasPathForPresentAndNotNull() {
+			assertThat(forJson(NULLS)).hasPath("$.valuename");
+		}
+
+		@Test
+		void hasPathForPresentAndNull() {
+			assertThat(forJson(NULLS)).hasPath("$.nullname");
+		}
+
+		@Test
+		void hasPathForOperatorMatching() {
+			assertThat(forJson(SIMPSONS)).
+					hasPath("$.familyMembers[?(@.name == 'Homer')]");
+		}
+
+		@Test
+		void hasPathForOperatorNotMatching() {
+			assertThat(forJson(SIMPSONS)).
+					hasPath("$.familyMembers[?(@.name == 'Dilbert')]");
+		}
+
+		@Test
+		void hasPathForNotPresent() {
+			String expression = "$.missing";
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(NULLS)).hasPath(expression))
+					.satisfies(hasFailedToMatchPath("$.missing"));
+		}
+
+		@Test
+		void hasPathSatisfying() {
+			assertThat(forJson(TYPES)).hasPathSatisfying("$.str", value -> assertThat(value).isEqualTo("foo"))
+					.hasPathSatisfying("$.num", value -> assertThat(value).isEqualTo(5));
+		}
+
+		@Test
+		void hasPathSatisfyingForPathNotPresent() {
+			String expression = "missing";
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(NULLS)).hasPathSatisfying(expression, value -> {}))
+					.satisfies(hasFailedToMatchPath(expression));
+		}
+
+		@Test
+		void doesNotHavePathForMissing() {
+			assertThat(forJson(NULLS)).doesNotHavePath("$.missing");
+		}
+
+
+		@Test
+		void doesNotHavePathForPresent() {
+			String expression = "$.valuename";
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(NULLS)).doesNotHavePath(expression))
+					.satisfies(hasFailedToNotMatchPath(expression));
+		}
+
+	}
+
+	@Nested
+	class ExtractingPathTests {
+
+		@Test
+		void extractingPathForNullJson() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(null)).extractingPath("$"))
+					.withMessageContaining("Expecting actual not to be null");
+		}
+
+		@Test
+		void isNullWithNullPathValue() {
+			assertThat(forJson(NULLS)).extractingPath("$.nullname").isNull();
+		}
+
+		@ParameterizedTest
+		@ValueSource(strings = { "$.str", "$.emptyString", "$.num", "$.bool", "$.arr",
+				"$.emptyArray", "$.colorMap", "$.emptyMap" })
+		void isNotNullWithValue(String path) {
+			assertThat(forJson(TYPES)).extractingPath(path).isNotNull();
+		}
+
+		@ParameterizedTest
+		@MethodSource
+		void isEqualToOnRawValue(String path, Object expected) {
+			assertThat(forJson(TYPES)).extractingPath(path).isEqualTo(expected);
+		}
+
+		static Stream isEqualToOnRawValue() {
+			return Stream.of(
+					Arguments.of("$.str", "foo"),
+					Arguments.of("$.num", 5),
+					Arguments.of("$.bool", true),
+					Arguments.of("$.arr", List.of(42)),
+					Arguments.of("$.colorMap", Map.of("red", "rojo")));
+		}
+
+		@Test
+		void asStringWithActualValue() {
+			assertThat(forJson(TYPES)).extractingPath("@.str").asString().startsWith("f").endsWith("o");
+		}
+
+		@Test
+		void asStringIsEmpty() {
+			assertThat(forJson(TYPES)).extractingPath("@.emptyString").asString().isEmpty();
+		}
+
+		@Test
+		void asNumberWithActualValue() {
+			assertThat(forJson(TYPES)).extractingPath("@.num").asNumber().isEqualTo(5);
+		}
+
+		@Test
+		void asBooleanWithActualValue() {
+			assertThat(forJson(TYPES)).extractingPath("@.bool").asBoolean().isTrue();
+		}
+
+		@Test
+		void asArrayWithActualValue() {
+			assertThat(forJson(TYPES)).extractingPath("@.arr").asArray().containsOnly(42);
+		}
+
+		@Test
+		void asArrayIsEmpty() {
+			assertThat(forJson(TYPES)).extractingPath("@.emptyArray").asArray().isEmpty();
+		}
+
+		@Test
+		void asArrayWithFilterPredicatesMatching() {
+			assertThat(forJson(SIMPSONS))
+					.extractingPath("$.familyMembers[?(@.name == 'Bart')]").asArray().hasSize(1);
+		}
+
+		@Test
+		void asArrayWithFilterPredicatesNotMatching() {
+			assertThat(forJson(SIMPSONS)).
+					extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").asArray().isEmpty();
+		}
+
+		@Test
+		void asMapWithActualValue() {
+			assertThat(forJson(TYPES)).extractingPath("@.colorMap").asMap().containsOnly(entry("red", "rojo"));
+		}
+
+		@Test
+		void asMapIsEmpty() {
+			assertThat(forJson(TYPES)).extractingPath("@.emptyMap").asMap().isEmpty();
+		}
+
+		@Test
+		void convertToWithoutHttpMessageConverterShouldFail() {
+			JsonPathValueAssert path = assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[0]");
+			assertThatIllegalStateException()
+					.isThrownBy(() -> path.convertTo(ExtractingPathTests.Member.class))
+					.withMessage("No JSON message converter available to convert {name=Homer}");
+		}
+
+		@Test
+		void convertToTargetType() {
+			assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
+					.extractingPath("$.familyMembers[0]").convertTo(ExtractingPathTests.Member.class)
+					.satisfies(member -> assertThat(member.name).isEqualTo("Homer"));
+		}
+
+		@Test
+		void convertToIncompatibleTargetTypeShouldFail() {
+			JsonPathValueAssert path = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
+					.extractingPath("$.familyMembers[0]");
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> path.convertTo(ExtractingPathTests.Customer.class))
+					.withMessageContainingAll("Expected value at JSON path \"$.familyMembers[0]\":",
+							Customer.class.getName(), "name");
+		}
+
+		@Test
+		void convertArrayToParameterizedType() {
+			assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
+					.extractingPath("$.familyMembers")
+					.convertTo(new ParameterizedTypeReference>() {})
+					.satisfies(family -> assertThat(family).hasSize(5).element(0).isEqualTo(new Member("Homer")));
+		}
+
+		@Test
+		void isEmptyWithPathHavingNullValue() {
+			assertThat(forJson(NULLS)).extractingPath("nullname").isEmpty();
+		}
+
+		@ParameterizedTest
+		@ValueSource(strings = { "$.emptyString", "$.emptyArray", "$.emptyMap" })
+		void isEmptyWithEmptyValue(String path) {
+			assertThat(forJson(TYPES)).extractingPath(path).isEmpty();
+		}
+
+		@Test
+		void isEmptyForPathWithFilterMatching() {
+			String expression = "$.familyMembers[?(@.name == 'Bart')]";
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isEmpty())
+					.withMessageContainingAll("Expected value at JSON path \"" + expression + "\"",
+							"[{\"name\":\"Bart\"}]", "To be empty");
+		}
+
+		@Test
+		void isEmptyForPathWithFilterNotMatching() {
+			assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").isEmpty();
+		}
+
+		@ParameterizedTest
+		@ValueSource(strings = { "$.str", "$.num", "$.bool", "$.arr", "$.colorMap" })
+		void isNotEmptyWithNonNullValue(String path) {
+			assertThat(forJson(TYPES)).extractingPath(path).isNotEmpty();
+		}
+
+		@Test
+		void isNotEmptyForPathWithFilterMatching() {
+			assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Bart')]").isNotEmpty();
+		}
+
+		@Test
+		void isNotEmptyForPathWithFilterNotMatching() {
+			String expression = "$.familyMembers[?(@.name == 'Dilbert')]";
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isNotEmpty())
+					.withMessageContainingAll("Expected value at JSON path \"" + expression + "\"",
+							"To not be empty");
+		}
+
+
+		private record Member(String name) {}
+
+		private record Customer(long id, String username) {}
+
+		private AssertProvider> forJson(@Nullable String json) {
+			return () -> new TestJsonContentAssert(json, null, null, null);
+		}
+
+		private AssertProvider> forJson(@Nullable String json, GenericHttpMessageConverter jsonHttpMessageConverter) {
+			return () -> new TestJsonContentAssert(json, jsonHttpMessageConverter, null, null);
+		}
+
+	}
+
+	@Nested
+	class EqualsNotEqualsTests {
+
+		@Test
+		void isEqualToWhenStringIsMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isEqualTo(SOURCE);
+		}
+
+		@Test
+		void isEqualToWhenNullActualShouldFail() {
+			assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
+					assertThat(forJson(null)).isEqualTo(SOURCE));
+		}
+
+		@Test
+		void isEqualToWhenExpectedIsNotAStringShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(SOURCE.getBytes()));
+		}
+	}
+
+	@Nested
+	@TestInstance(Lifecycle.PER_CLASS)
+	class JsonAssertTests {
+
+		@Test
+		void isEqualToWhenExpectedIsNullShouldFail() {
+			CharSequence actual = null;
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(actual, JSONCompareMode.LENIENT));
+		}
+
+		@Test
+		void isEqualToWhenStringIsMatchingAndLenientShouldPass() {
+			assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT);
+		}
+
+		@Test
+		void isEqualToWhenStringIsNotMatchingAndLenientShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, JSONCompareMode.LENIENT));
+		}
+
+		@Test
+		void isEqualToWhenResourcePathIsMatchingAndLenientShouldPass() {
+			assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", JSONCompareMode.LENIENT);
+		}
+
+		@Test
+		void isEqualToWhenResourcePathIsNotMatchingAndLenientShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", JSONCompareMode.LENIENT));
+		}
+
+		Stream source() {
+			return Stream.of(
+					Arguments.of(new ClassPathResource("source.json", AbstractJsonContentAssertTests.class)),
+					Arguments.of(new ByteArrayResource(SOURCE.getBytes())),
+					Arguments.of(new FileSystemResource(createFile(SOURCE))),
+					Arguments.of(new InputStreamResource(createInputStream(SOURCE))));
+		}
+
+		Stream lenientSame() {
+			return Stream.of(
+					Arguments.of(new ClassPathResource("lenient-same.json", AbstractJsonContentAssertTests.class)),
+					Arguments.of(new ByteArrayResource(LENIENT_SAME.getBytes())),
+					Arguments.of(new FileSystemResource(createFile(LENIENT_SAME))),
+					Arguments.of(new InputStreamResource(createInputStream(LENIENT_SAME))));
+		}
+
+		Stream different() {
+			return Stream.of(
+					Arguments.of(new ClassPathResource("different.json", AbstractJsonContentAssertTests.class)),
+					Arguments.of(new ByteArrayResource(DIFFERENT.getBytes())),
+					Arguments.of(new FileSystemResource(createFile(DIFFERENT))),
+					Arguments.of(new InputStreamResource(createInputStream(DIFFERENT))));
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isEqualToWhenResourceIsMatchingAndLenientSameShouldPass(Resource expected) {
+			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));
+		}
+
+		@Test
+		void isEqualToWhenStringIsMatchingAndComparatorShouldPass() {
+			assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, comparator);
+		}
+
+		@Test
+		void isEqualToWhenStringIsNotMatchingAndComparatorShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, comparator));
+		}
+
+		@Test
+		void isEqualToWhenResourcePathIsMatchingAndComparatorShouldPass() {
+			assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", comparator);
+		}
+
+		@Test
+		void isEqualToWhenResourcePathIsNotMatchingAndComparatorShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", comparator));
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isEqualToWhenResourceIsMatchingAndComparatorShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isEqualTo(expected, comparator);
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isEqualToWhenResourceIsNotMatchingAndComparatorShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(expected, comparator));
+		}
+
+		@Test
+		void isLenientlyEqualToWhenStringIsMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isLenientlyEqualTo(LENIENT_SAME);
+		}
+
+		@Test
+		void isLenientlyEqualToWhenNullActualShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(null)).isLenientlyEqualTo(SOURCE));
+		}
+
+		@Test
+		void isLenientlyEqualToWhenStringIsNotMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(DIFFERENT));
+		}
+
+		@Test
+		void isLenientlyEqualToWhenExpectedDoesNotExistShouldFail() {
+			assertThatIllegalStateException()
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("does-not-exist.json"))
+					.withMessage("Unable to load JSON from class path resource [org/springframework/test/json/does-not-exist.json]");
+		}
+
+		@Test
+		void isLenientlyEqualToWhenResourcePathIsMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isLenientlyEqualTo("lenient-same.json");
+		}
+
+		@Test
+		void isLenientlyEqualToWhenResourcePathIsNotMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("different.json"));
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isLenientlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected);
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isLenientlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected));
+		}
+
+		@Test
+		void isStrictlyEqualToWhenStringIsMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isStrictlyEqualTo(SOURCE);
+		}
+
+		@Test
+		void isStrictlyEqualToWhenStringIsNotMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(LENIENT_SAME));
+		}
+
+		@Test
+		void isStrictlyEqualToWhenResourcePathIsMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isStrictlyEqualTo("source.json");
+		}
+
+		@Test
+		void isStrictlyEqualToWhenResourcePathIsNotMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo("lenient-same.json"));
+		}
+
+		@ParameterizedTest
+		@MethodSource("source")
+		void isStrictlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected);
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isStrictlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected));
+		}
+
+
+		@Test
+		void isNotEqualToWhenStringIsMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE));
+		}
+
+		@Test
+		void isNotEqualToWhenNullActualShouldPass() {
+			assertThat(forJson(null)).isNotEqualTo(SOURCE);
+		}
+
+		@Test
+		void isNotEqualToWhenStringIsNotMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT);
+		}
+
+		@Test
+		void isNotEqualToAsObjectWhenExpectedIsNotAStringShouldNotFail() {
+			assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE.getBytes());
+		}
+
+		@Test
+		void isNotEqualToWhenStringIsMatchingAndLenientShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT));
+		}
+
+		@Test
+		void isNotEqualToWhenStringIsNotMatchingAndLenientShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, JSONCompareMode.LENIENT);
+		}
+
+		@Test
+		void isNotEqualToWhenResourcePathIsMatchingAndLenientShouldFail() {
+			assertThatExceptionOfType(AssertionError.class).isThrownBy(
+					() -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", JSONCompareMode.LENIENT));
+		}
+
+		@Test
+		void isNotEqualToWhenResourcePathIsNotMatchingAndLenientShouldPass() {
+			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));
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isNotEqualToWhenResourceIsNotMatchingAndLenientShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isNotEqualTo(expected, JSONCompareMode.LENIENT);
+		}
+
+		@Test
+		void isNotEqualToWhenStringIsMatchingAndComparatorShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, comparator));
+		}
+
+		@Test
+		void isNotEqualToWhenStringIsNotMatchingAndComparatorShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, comparator);
+		}
+
+		@Test
+		void isNotEqualToWhenResourcePathIsMatchingAndComparatorShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", comparator));
+		}
+
+		@Test
+		void isNotEqualToWhenResourcePathIsNotMatchingAndComparatorShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo("different.json", comparator);
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class).isThrownBy(
+					() -> assertThat(forJson(SOURCE)).isNotEqualTo(expected, comparator));
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isNotEqualTo(expected, comparator);
+		}
+
+		@Test
+		void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(createResource(LENIENT_SAME), comparator));
+		}
+
+		@Test
+		void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo(createResource(DIFFERENT), comparator);
+		}
+
+		@Test
+		void isNotLenientlyEqualToWhenNullActualShouldPass() {
+			assertThat(forJson(null)).isNotLenientlyEqualTo(SOURCE);
+		}
+
+		@Test
+		void isNotLenientlyEqualToWhenStringIsNotMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(DIFFERENT);
+		}
+
+		@Test
+		void isNotLenientlyEqualToWhenResourcePathIsMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("lenient-same.json"));
+		}
+
+		@Test
+		void isNotLenientlyEqualToWhenResourcePathIsNotMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("different.json");
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isNotLenientlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected));
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isNotLenientlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected);
+		}
+
+		@Test
+		void isNotStrictlyEqualToWhenStringIsMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(SOURCE));
+		}
+
+		@Test
+		void isNotStrictlyEqualToWhenStringIsNotMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(LENIENT_SAME);
+		}
+
+		@Test
+		void isNotStrictlyEqualToWhenResourcePathIsMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("source.json"));
+		}
+
+		@Test
+		void isNotStrictlyEqualToWhenResourcePathIsNotMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("lenient-same.json");
+		}
+
+		@ParameterizedTest
+		@MethodSource("source")
+		void isNotStrictlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected));
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isNotStrictlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected);
+		}
+
+		private AssertProvider> forJson(@Nullable String json) {
+			return () -> new TestJsonContentAssert(json, null, getClass(), null);
+		}
+
+	}
+
+
+	private Consumer hasFailedToMatchPath(String expression) {
+		return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
+				"To match JSON path:", "\"" + expression + "\"");
+	}
+
+	private Consumer hasFailedToNotMatchPath(String expression) {
+		return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
+				"Not to match JSON path:", "\"" + expression + "\"");
+	}
+
+	private Path createFile(String content) {
+		try {
+			Path temp = Files.createTempFile("file", ".json");
+			Files.writeString(temp, content);
+			return temp;
+		}
+		catch (IOException ex) {
+			throw new IllegalStateException(ex);
+		}
+	}
+
+	private InputStream createInputStream(String content) {
+		return new ByteArrayInputStream(content.getBytes());
+	}
+
+	private Resource createResource(String content) {
+		return new ByteArrayResource(content.getBytes());
+	}
+
+	private static String loadJson(String path) {
+		try {
+			ClassPathResource resource = new ClassPathResource(path, AbstractJsonContentAssertTests.class);
+			return new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
+		}
+		catch (Exception ex) {
+			throw new IllegalStateException(ex);
+		}
+
+	}
+
+	private AssertProvider> forJson(@Nullable String json) {
+		return () -> new TestJsonContentAssert(json, null, null, null);
+	}
+
+	private static class TestJsonContentAssert extends AbstractJsonContentAssert {
+
+		public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter, @Nullable Class resourceLoadClass, @Nullable Charset charset) {
+			super(json, jsonMessageConverter, resourceLoadClass, charset, TestJsonContentAssert.class);
+		}
+	}
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java
deleted file mode 100644
index 02c839bd8e0..00000000000
--- a/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java
+++ /dev/null
@@ -1,479 +0,0 @@
-/*
- * 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 java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.stream.Stream;
-
-import org.assertj.core.api.AssertProvider;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestInstance;
-import org.junit.jupiter.api.TestInstance.Lifecycle;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import org.skyscreamer.jsonassert.JSONCompareMode;
-import org.skyscreamer.jsonassert.comparator.DefaultComparator;
-import org.skyscreamer.jsonassert.comparator.JSONComparator;
-
-import org.springframework.core.io.ByteArrayResource;
-import org.springframework.core.io.ClassPathResource;
-import org.springframework.core.io.FileSystemResource;
-import org.springframework.core.io.InputStreamResource;
-import org.springframework.core.io.Resource;
-import org.springframework.lang.Nullable;
-import org.springframework.util.FileCopyUtils;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
-
-/**
- * Tests for {@link JsonContentAssert}.
- *
- * @author Stephane Nicoll
- * @author Phillip Webb
- */
-@TestInstance(Lifecycle.PER_CLASS)
-class JsonContentAssertTests {
-
-	private static final String SOURCE = loadJson("source.json");
-
-	private static final String LENIENT_SAME = loadJson("lenient-same.json");
-
-	private static final String DIFFERENT = loadJson("different.json");
-
-	private static final JSONComparator COMPARATOR = new DefaultComparator(JSONCompareMode.LENIENT);
-
-	@Test
-	void isEqualToWhenStringIsMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isEqualTo(SOURCE);
-	}
-
-	@Test
-	void isEqualToWhenNullActualShouldFail() {
-		assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
-				assertThat(forJson(null)).isEqualTo(SOURCE));
-	}
-
-	@Test
-	void isEqualToWhenExpectedIsNotAStringShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(SOURCE.getBytes()));
-	}
-
-	@Test
-	void isEqualToWhenExpectedIsNullShouldFail() {
-		CharSequence actual = null;
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(actual, JSONCompareMode.LENIENT));
-	}
-
-	@Test
-	void isEqualToWhenStringIsMatchingAndLenientShouldPass() {
-		assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT);
-	}
-
-	@Test
-	void isEqualToWhenStringIsNotMatchingAndLenientShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, JSONCompareMode.LENIENT));
-	}
-
-	@Test
-	void isEqualToWhenResourcePathIsMatchingAndLenientShouldPass() {
-		assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", JSONCompareMode.LENIENT);
-	}
-
-	@Test
-	void isEqualToWhenResourcePathIsNotMatchingAndLenientShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", JSONCompareMode.LENIENT));
-	}
-
-	Stream source() {
-		return Stream.of(
-				Arguments.of(new ClassPathResource("source.json", JsonContentAssertTests.class)),
-				Arguments.of(new ByteArrayResource(SOURCE.getBytes())),
-				Arguments.of(new FileSystemResource(createFile(SOURCE))),
-				Arguments.of(new InputStreamResource(createInputStream(SOURCE))));
-	}
-
-	Stream lenientSame() {
-		return Stream.of(
-				Arguments.of(new ClassPathResource("lenient-same.json", JsonContentAssertTests.class)),
-				Arguments.of(new ByteArrayResource(LENIENT_SAME.getBytes())),
-				Arguments.of(new FileSystemResource(createFile(LENIENT_SAME))),
-				Arguments.of(new InputStreamResource(createInputStream(LENIENT_SAME))));
-	}
-
-	Stream different() {
-		return Stream.of(
-				Arguments.of(new ClassPathResource("different.json", JsonContentAssertTests.class)),
-				Arguments.of(new ByteArrayResource(DIFFERENT.getBytes())),
-				Arguments.of(new FileSystemResource(createFile(DIFFERENT))),
-				Arguments.of(new InputStreamResource(createInputStream(DIFFERENT))));
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isEqualToWhenResourceIsMatchingAndLenientSameShouldPass(Resource expected) {
-		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));
-	}
-
-
-	@Test
-	void isEqualToWhenStringIsMatchingAndComparatorShouldPass() {
-		assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, COMPARATOR);
-	}
-
-	@Test
-	void isEqualToWhenStringIsNotMatchingAndComparatorShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, COMPARATOR));
-	}
-
-	@Test
-	void isEqualToWhenResourcePathIsMatchingAndComparatorShouldPass() {
-		assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", COMPARATOR);
-	}
-
-	@Test
-	void isEqualToWhenResourcePathIsNotMatchingAndComparatorShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", COMPARATOR));
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isEqualToWhenResourceIsMatchingAndComparatorShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isEqualTo(expected, COMPARATOR);
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isEqualToWhenResourceIsNotMatchingAndComparatorShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(expected, COMPARATOR));
-	}
-
-	@Test
-	void isLenientlyEqualToWhenStringIsMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isLenientlyEqualTo(LENIENT_SAME);
-	}
-
-	@Test
-	void isLenientlyEqualToWhenNullActualShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(null)).isLenientlyEqualTo(SOURCE));
-	}
-
-	@Test
-	void isLenientlyEqualToWhenStringIsNotMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(DIFFERENT));
-	}
-
-	@Test
-	void isLenientlyEqualToWhenExpectedDoesNotExistShouldFail() {
-		assertThatIllegalStateException()
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("does-not-exist.json"))
-				.withMessage("Unable to load JSON from class path resource [org/springframework/test/json/does-not-exist.json]");
-	}
-
-	@Test
-	void isLenientlyEqualToWhenResourcePathIsMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isLenientlyEqualTo("lenient-same.json");
-	}
-
-	@Test
-	void isLenientlyEqualToWhenResourcePathIsNotMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("different.json"));
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isLenientlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected);
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isLenientlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected));
-	}
-
-	@Test
-	void isStrictlyEqualToWhenStringIsMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isStrictlyEqualTo(SOURCE);
-	}
-
-	@Test
-	void isStrictlyEqualToWhenStringIsNotMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(LENIENT_SAME));
-	}
-
-	@Test
-	void isStrictlyEqualToWhenResourcePathIsMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isStrictlyEqualTo("source.json");
-	}
-
-	@Test
-	void isStrictlyEqualToWhenResourcePathIsNotMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo("lenient-same.json"));
-	}
-
-	@ParameterizedTest
-	@MethodSource("source")
-	void isStrictlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected);
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isStrictlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected));
-	}
-
-
-	@Test
-	void isNotEqualToWhenStringIsMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE));
-	}
-
-	@Test
-	void isNotEqualToWhenNullActualShouldPass() {
-		assertThat(forJson(null)).isNotEqualTo(SOURCE);
-	}
-
-	@Test
-	void isNotEqualToWhenStringIsNotMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT);
-	}
-
-	@Test
-	void isNotEqualToAsObjectWhenExpectedIsNotAStringShouldNotFail() {
-		assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE.getBytes());
-	}
-
-	@Test
-	void isNotEqualToWhenStringIsMatchingAndLenientShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT));
-	}
-
-	@Test
-	void isNotEqualToWhenStringIsNotMatchingAndLenientShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, JSONCompareMode.LENIENT);
-	}
-
-	@Test
-	void isNotEqualToWhenResourcePathIsMatchingAndLenientShouldFail() {
-		assertThatExceptionOfType(AssertionError.class).isThrownBy(
-				() -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", JSONCompareMode.LENIENT));
-	}
-
-	@Test
-	void isNotEqualToWhenResourcePathIsNotMatchingAndLenientShouldPass() {
-		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));
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isNotEqualToWhenResourceIsNotMatchingAndLenientShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isNotEqualTo(expected, JSONCompareMode.LENIENT);
-	}
-
-	@Test
-	void isNotEqualToWhenStringIsMatchingAndComparatorShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, COMPARATOR));
-	}
-
-	@Test
-	void isNotEqualToWhenStringIsNotMatchingAndComparatorShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, COMPARATOR);
-	}
-
-	@Test
-	void isNotEqualToWhenResourcePathIsMatchingAndComparatorShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", COMPARATOR));
-	}
-
-	@Test
-	void isNotEqualToWhenResourcePathIsNotMatchingAndComparatorShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo("different.json", COMPARATOR);
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class).isThrownBy(
-				() -> assertThat(forJson(SOURCE)).isNotEqualTo(expected, COMPARATOR));
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isNotEqualTo(expected, COMPARATOR);
-	}
-
-	@Test
-	void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(createResource(LENIENT_SAME), COMPARATOR));
-	}
-
-	@Test
-	void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo(createResource(DIFFERENT), COMPARATOR);
-	}
-
-	@Test
-	void isNotLenientlyEqualToWhenNullActualShouldPass() {
-		assertThat(forJson(null)).isNotLenientlyEqualTo(SOURCE);
-	}
-
-	@Test
-	void isNotLenientlyEqualToWhenStringIsNotMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(DIFFERENT);
-	}
-
-	@Test
-	void isNotLenientlyEqualToWhenResourcePathIsMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("lenient-same.json"));
-	}
-
-	@Test
-	void isNotLenientlyEqualToWhenResourcePathIsNotMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("different.json");
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isNotLenientlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected));
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isNotLenientlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected);
-	}
-
-	@Test
-	void isNotStrictlyEqualToWhenStringIsMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(SOURCE));
-	}
-
-	@Test
-	void isNotStrictlyEqualToWhenStringIsNotMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(LENIENT_SAME);
-	}
-
-	@Test
-	void isNotStrictlyEqualToWhenResourcePathIsMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("source.json"));
-	}
-
-	@Test
-	void isNotStrictlyEqualToWhenResourcePathIsNotMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("lenient-same.json");
-	}
-
-	@ParameterizedTest
-	@MethodSource("source")
-	void isNotStrictlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected));
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isNotStrictlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected);
-	}
-
-	@Test
-	void isNullWhenActualIsNullShouldPass() {
-		assertThat(forJson(null)).isNull();
-	}
-
-	private Path createFile(String content) {
-		try {
-			Path temp = Files.createTempFile("file", ".json");
-			Files.writeString(temp, content);
-			return temp;
-		}
-		catch (IOException ex) {
-			throw new IllegalStateException(ex);
-		}
-	}
-
-	private InputStream createInputStream(String content) {
-		return new ByteArrayInputStream(content.getBytes());
-	}
-
-	private Resource createResource(String content) {
-		return new ByteArrayResource(content.getBytes());
-	}
-
-	private static String loadJson(String path) {
-		try {
-			ClassPathResource resource = new ClassPathResource(path, JsonContentAssertTests.class);
-			return new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
-		}
-		catch (Exception ex) {
-			throw new IllegalStateException(ex);
-		}
-
-	}
-
-	private AssertProvider forJson(@Nullable String json) {
-		return () -> new JsonContentAssert(json, JsonContentAssertTests.class);
-	}
-
-}
diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java
deleted file mode 100644
index d1a89686283..00000000000
--- a/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * 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 java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.stream.Stream;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.assertj.core.api.AssertProvider;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import org.junit.jupiter.params.provider.ValueSource;
-
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.core.io.ClassPathResource;
-import org.springframework.http.converter.GenericHttpMessageConverter;
-import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
-import org.springframework.lang.Nullable;
-import org.springframework.util.FileCopyUtils;
-
-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;
-
-/**
- * Tests for {@link JsonPathAssert}.
- *
- * @author Phillip Webb
- * @author Stephane Nicoll
- */
-class JsonPathAssertTests {
-
-	private static final String TYPES = loadJson("types.json");
-
-	private static final String SIMPSONS = loadJson("simpsons.json");
-
-	private static final String NULLS = loadJson("nulls.json");
-
-	private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter =
-			new MappingJackson2HttpMessageConverter(new ObjectMapper());
-
-
-	@Nested
-	class HasPathTests {
-
-		@Test
-		void hasPathForPresentAndNotNull() {
-			assertThat(forJson(NULLS)).hasPath("$.valuename");
-		}
-
-		@Test
-		void hasPathForPresentAndNull() {
-			assertThat(forJson(NULLS)).hasPath("$.nullname");
-		}
-
-		@Test
-		void hasPathForOperatorMatching() {
-			assertThat(forJson(SIMPSONS)).
-					hasPath("$.familyMembers[?(@.name == 'Homer')]");
-		}
-
-		@Test
-		void hasPathForOperatorNotMatching() {
-			assertThat(forJson(SIMPSONS)).
-					hasPath("$.familyMembers[?(@.name == 'Dilbert')]");
-		}
-
-		@Test
-		void hasPathForNotPresent() {
-			String expression = "$.missing";
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> assertThat(forJson(NULLS)).hasPath(expression))
-					.satisfies(hasFailedToMatchPath("$.missing"));
-		}
-
-		@Test
-		void hasPathSatisfying() {
-			assertThat(forJson(TYPES)).hasPathSatisfying("$.str", value -> assertThat(value).isEqualTo("foo"))
-					.hasPathSatisfying("$.num", value -> assertThat(value).isEqualTo(5));
-		}
-
-		@Test
-		void hasPathSatisfyingForPathNotPresent() {
-			String expression = "missing";
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> assertThat(forJson(NULLS)).hasPathSatisfying(expression, value -> {}))
-					.satisfies(hasFailedToMatchPath(expression));
-		}
-
-		@Test
-		void doesNotHavePathForMissing() {
-			assertThat(forJson(NULLS)).doesNotHavePath("$.missing");
-		}
-
-
-		@Test
-		void doesNotHavePathForPresent() {
-			String expression = "$.valuename";
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> assertThat(forJson(NULLS)).doesNotHavePath(expression))
-					.satisfies(hasFailedToNotMatchPath(expression));
-		}
-	}
-
-
-	@Nested
-	class ExtractingPathTests {
-
-		@Test
-		void isNullWithNullPathValue() {
-			assertThat(forJson(NULLS)).extractingPath("$.nullname").isNull();
-		}
-
-		@ParameterizedTest
-		@ValueSource(strings = { "$.str", "$.emptyString", "$.num", "$.bool", "$.arr",
-				"$.emptyArray", "$.colorMap", "$.emptyMap" })
-		void isNotNullWithValue(String path) {
-			assertThat(forJson(TYPES)).extractingPath(path).isNotNull();
-		}
-
-		@ParameterizedTest
-		@MethodSource
-		void isEqualToOnRawValue(String path, Object expected) {
-			assertThat(forJson(TYPES)).extractingPath(path).isEqualTo(expected);
-		}
-
-		static Stream isEqualToOnRawValue() {
-			return Stream.of(
-					Arguments.of("$.str", "foo"),
-					Arguments.of("$.num", 5),
-					Arguments.of("$.bool", true),
-					Arguments.of("$.arr", List.of(42)),
-					Arguments.of("$.colorMap", Map.of("red", "rojo")));
-		}
-
-		@Test
-		void asStringWithActualValue() {
-			assertThat(forJson(TYPES)).extractingPath("@.str").asString().startsWith("f").endsWith("o");
-		}
-
-		@Test
-		void asStringIsEmpty() {
-			assertThat(forJson(TYPES)).extractingPath("@.emptyString").asString().isEmpty();
-		}
-
-		@Test
-		void asNumberWithActualValue() {
-			assertThat(forJson(TYPES)).extractingPath("@.num").asNumber().isEqualTo(5);
-		}
-
-		@Test
-		void asBooleanWithActualValue() {
-			assertThat(forJson(TYPES)).extractingPath("@.bool").asBoolean().isTrue();
-		}
-
-		@Test
-		void asArrayWithActualValue() {
-			assertThat(forJson(TYPES)).extractingPath("@.arr").asArray().containsOnly(42);
-		}
-
-		@Test
-		void asArrayIsEmpty() {
-			assertThat(forJson(TYPES)).extractingPath("@.emptyArray").asArray().isEmpty();
-		}
-
-		@Test
-		void asArrayWithFilterPredicatesMatching() {
-			assertThat(forJson(SIMPSONS))
-					.extractingPath("$.familyMembers[?(@.name == 'Bart')]").asArray().hasSize(1);
-		}
-
-		@Test
-		void asArrayWithFilterPredicatesNotMatching() {
-			assertThat(forJson(SIMPSONS)).
-					extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").asArray().isEmpty();
-		}
-
-		@Test
-		void asMapWithActualValue() {
-			assertThat(forJson(TYPES)).extractingPath("@.colorMap").asMap().containsOnly(entry("red", "rojo"));
-		}
-
-		@Test
-		void asMapIsEmpty() {
-			assertThat(forJson(TYPES)).extractingPath("@.emptyMap").asMap().isEmpty();
-		}
-
-		@Test
-		void convertToWithoutHttpMessageConverterShouldFail() {
-			JsonPathValueAssert path = assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[0]");
-			assertThatIllegalStateException()
-					.isThrownBy(() -> path.convertTo(Member.class))
-					.withMessage("No JSON message converter available to convert {name=Homer}");
-		}
-
-		@Test
-		void convertToTargetType() {
-			assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
-					.extractingPath("$.familyMembers[0]").convertTo(Member.class)
-					.satisfies(member -> assertThat(member.name).isEqualTo("Homer"));
-		}
-
-		@Test
-		void convertToIncompatibleTargetTypeShouldFail() {
-			JsonPathValueAssert path = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
-					.extractingPath("$.familyMembers[0]");
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> path.convertTo(Customer.class))
-					.withMessageContainingAll("Expected value at JSON path \"$.familyMembers[0]\":",
-							Customer.class.getName(), "name");
-		}
-
-		@Test
-		void convertArrayToParameterizedType() {
-			assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
-					.extractingPath("$.familyMembers")
-					.convertTo(new ParameterizedTypeReference>() {})
-					.satisfies(family -> assertThat(family).hasSize(5).element(0).isEqualTo(new Member("Homer")));
-		}
-
-		@Test
-		void isEmptyWithPathHavingNullValue() {
-			assertThat(forJson(NULLS)).extractingPath("nullname").isEmpty();
-		}
-
-		@ParameterizedTest
-		@ValueSource(strings = { "$.emptyString", "$.emptyArray", "$.emptyMap" })
-		void isEmptyWithEmptyValue(String path) {
-			assertThat(forJson(TYPES)).extractingPath(path).isEmpty();
-		}
-
-		@Test
-		void isEmptyForPathWithFilterMatching() {
-			String expression = "$.familyMembers[?(@.name == 'Bart')]";
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isEmpty())
-					.withMessageContainingAll("Expected value at JSON path \"" + expression + "\"",
-							"[{\"name\":\"Bart\"}]", "To be empty");
-		}
-
-		@Test
-		void isEmptyForPathWithFilterNotMatching() {
-			assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").isEmpty();
-		}
-
-		@ParameterizedTest
-		@ValueSource(strings = { "$.str", "$.num", "$.bool", "$.arr", "$.colorMap" })
-		void isNotEmptyWithNonNullValue(String path) {
-			assertThat(forJson(TYPES)).extractingPath(path).isNotEmpty();
-		}
-
-		@Test
-		void isNotEmptyForPathWithFilterMatching() {
-			assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Bart')]").isNotEmpty();
-		}
-
-		@Test
-		void isNotEmptyForPathWithFilterNotMatching() {
-			String expression = "$.familyMembers[?(@.name == 'Dilbert')]";
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isNotEmpty())
-					.withMessageContainingAll("Expected value at JSON path \"" + expression + "\"",
-							"To not be empty");
-		}
-
-
-		private record Member(String name) {}
-
-		private record Customer(long id, String username) {}
-
-	}
-
-	private Consumer hasFailedToMatchPath(String expression) {
-		return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
-				"To match JSON path:", "\"" + expression + "\"");
-	}
-
-	private Consumer hasFailedToNotMatchPath(String expression) {
-		return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
-				"Not to match JSON path:", "\"" + expression + "\"");
-	}
-
-
-	private static String loadJson(String path) {
-		try {
-			ClassPathResource resource = new ClassPathResource(path, JsonPathAssertTests.class);
-			return new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
-		}
-		catch (Exception ex) {
-			throw new IllegalStateException(ex);
-		}
-	}
-
-	private AssertProvider forJson(String json) {
-		return forJson(json, null);
-	}
-
-	private AssertProvider forJson(String json,
-			@Nullable GenericHttpMessageConverter jsonHttpMessageConverter) {
-		return () -> new JsonPathAssert(json, jsonHttpMessageConverter);
-	}
-
-}
diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java
index c86ae122d79..4baa08b154e 100644
--- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java
+++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java
@@ -31,7 +31,7 @@ import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
 import org.springframework.mock.web.MockServletContext;
-import org.springframework.test.json.JsonPathAssert;
+import org.springframework.test.json.AbstractJsonContentAssert;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -99,9 +99,9 @@ class AssertableMockMvcTests {
 	@Test
 	void createWithControllersHasNoHttpMessageConverter() {
 		AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController());
-		JsonPathAssert jsonPathAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath();
+		AbstractJsonContentAssert jsonContentAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath();
 		assertThatIllegalStateException()
-				.isThrownBy(() -> jsonPathAssert.extractingPath("$").convertTo(Message.class))
+				.isThrownBy(() -> jsonContentAssert.extractingPath("$").convertTo(Message.class))
 				.withMessageContaining("No JSON message converter available");
 	}