diff --git a/pom.xml b/pom.xml index 7f9e00a..2fc91a8 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.archyoshi assertj-json - 0.1.2 + 0.1.3-SNAPSHOT jar AssertJ JSON diff --git a/src/main/java/com/archyoshi/assertj/json/JsonAssertions.java b/src/main/java/com/archyoshi/assertj/json/JsonAssertions.java index bae8de6..7c850d6 100644 --- a/src/main/java/com/archyoshi/assertj/json/JsonAssertions.java +++ b/src/main/java/com/archyoshi/assertj/json/JsonAssertions.java @@ -18,9 +18,10 @@ import com.archyoshi.assertj.json.api.JsonComparisonAssert; import com.archyoshi.assertj.json.api.JsonIterableAssert; import com.archyoshi.assertj.json.api.JsonNodeAssert; -import com.fasterxml.jackson.core.JsonProcessingException; +import com.archyoshi.assertj.json.api.JsonNodeLoader; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; import java.nio.file.Path; /** @@ -83,7 +84,7 @@ public static JsonNodeAssert assertThat(final String json) { * @return a new {@link JsonNodeAssert} instance */ public static JsonNodeAssert assertThat(final String json, final ObjectMapper mapper) { - return new JsonNodeAssert(parseJson(json, mapper), mapper); + return new JsonNodeAssert(JsonNodeLoader.toNode(json, mapper), mapper); } /** @@ -92,7 +93,7 @@ public static JsonNodeAssert assertThat(final String json, final ObjectMapper ma *

The file is read and parsed as JSON content. The default {@code ObjectMapper} is used. * * @param jsonFile the path to the JSON file to assert on - * @return a new {@link JsonNodeAssert} instance + * @return a new {@link JsonComparisonAssert} instance * @throws AssertionError if the file cannot be read or contains invalid JSON * @since 0.1.0 */ @@ -111,6 +112,35 @@ public static JsonComparisonAssert assertThat(final Path jsonFile, final ObjectM return new JsonComparisonAssert(jsonFile, mapper); } + /** + * Creates a new assertion object for the given JSON file. + * + *

The file is read and parsed as JSON content. The default {@code ObjectMapper} is used. + * + * @param jsonFile the JSON file to assert on + * @return a new {@link JsonComparisonAssert} instance + * @throws AssertionError if the file cannot be read or contains invalid JSON + * @since 0.1.3 + */ + public static JsonComparisonAssert assertThat(final File jsonFile) { + return assertThat(jsonFile, MAPPER); + } + + /** + * Creates an assertion object for a JSON file parsed with the supplied mapper. + * + * @param jsonFile the JSON file to assert on + * @param mapper the mapper used to parse JSON + * @return a new {@link JsonComparisonAssert} instance + * @since 0.1.3 + */ + public static JsonComparisonAssert assertThat(final File jsonFile, final ObjectMapper mapper) { + if (jsonFile == null) { + throw new AssertionError("Expected JSON file not to be null"); + } + return assertThat(jsonFile.toPath(), mapper); + } + /** * Creates iterable assertions for a JSON array node. * @@ -123,11 +153,82 @@ public static JsonIterableAssert assertThatArray(final JsonNode actual) { return JsonIterableAssert.assertThat(actual); } - private static JsonNode parseJson(final String json, final ObjectMapper mapper) { - try { - return mapper.readTree(json); - } catch (JsonProcessingException e) { - throw new AssertionError("Invalid JSON content: " + e.getOriginalMessage(), e); - } + /** + * Creates iterable assertions for a JSON array string. + * + * @param jsonArray the JSON array string + * @return an assertion object for the array elements + * @throws AssertionError if the string cannot be parsed or is not a JSON array + * @since 0.1.3 + */ + public static JsonIterableAssert assertThatArray(final String jsonArray) { + return assertThatArray(jsonArray, MAPPER); + } + + /** + * Creates iterable assertions for a JSON array string parsed with the supplied mapper. + * + * @param jsonArray the JSON array string + * @param mapper the mapper used to parse the string + * @return an assertion object for the array elements + * @throws AssertionError if the string cannot be parsed or is not a JSON array + * @since 0.1.3 + */ + public static JsonIterableAssert assertThatArray( + final String jsonArray, final ObjectMapper mapper) { + return assertThatArray(JsonNodeLoader.toNode(jsonArray, mapper)); + } + + /** + * Creates iterable assertions for a JSON array file at the given path. + * + * @param jsonArrayPath the path to the JSON array file + * @return an assertion object for the array elements + * @throws AssertionError if the file cannot be read or is not a JSON array + * @since 0.1.3 + */ + public static JsonIterableAssert assertThatArray(final Path jsonArrayPath) { + return assertThatArray(jsonArrayPath, MAPPER); + } + + /** + * Creates iterable assertions for a JSON array file at the given path using the supplied + * mapper. + * + * @param jsonArrayPath the path to the JSON array file + * @param mapper the mapper used to parse JSON + * @return an assertion object for the array elements + * @throws AssertionError if the file cannot be read or is not a JSON array + * @since 0.1.3 + */ + public static JsonIterableAssert assertThatArray( + final Path jsonArrayPath, final ObjectMapper mapper) { + return assertThatArray(JsonNodeLoader.toNode(jsonArrayPath, mapper)); + } + + /** + * Creates iterable assertions for a JSON array file. + * + * @param jsonArrayFile the JSON array file + * @return an assertion object for the array elements + * @throws AssertionError if the file cannot be read or is not a JSON array + * @since 0.1.3 + */ + public static JsonIterableAssert assertThatArray(final File jsonArrayFile) { + return assertThatArray(jsonArrayFile, MAPPER); + } + + /** + * Creates iterable assertions for a JSON array file using the supplied mapper. + * + * @param jsonArrayFile the JSON array file + * @param mapper the mapper used to parse JSON + * @return an assertion object for the array elements + * @throws AssertionError if the file cannot be read or is not a JSON array + * @since 0.1.3 + */ + public static JsonIterableAssert assertThatArray( + final File jsonArrayFile, final ObjectMapper mapper) { + return assertThatArray(JsonNodeLoader.toNode(jsonArrayFile, mapper)); } } diff --git a/src/main/java/com/archyoshi/assertj/json/api/JsonComparisonAssert.java b/src/main/java/com/archyoshi/assertj/json/api/JsonComparisonAssert.java index 4d83b97..fa2b2b0 100644 --- a/src/main/java/com/archyoshi/assertj/json/api/JsonComparisonAssert.java +++ b/src/main/java/com/archyoshi/assertj/json/api/JsonComparisonAssert.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -61,6 +62,23 @@ public JsonComparisonAssert(final Path actual, final ObjectMapper mapper) { this.mapper = mapper; } + /** + * Verifies that the actual JSON file has the same field names as the expected JSON node. Values + * are ignored, while nested object structure and array positions are preserved. + * + * @param expected the expected JSON node + * @return this assertion object + * @since 0.1.3 + */ + public JsonComparisonAssert hasSameFieldsAs(final JsonNode expected) { + final JsonNode actualNode = actualJson(); + if (!sameFields(actualNode, expected)) { + failWithActualExpectedAndMessage( + actualNode, expected, "Expected JSON files to have the same fields"); + } + return this; + } + /** * Verifies that the actual JSON file and expected JSON file contain the same field names. * Values are ignored, while nested object structure and array positions are preserved. @@ -69,7 +87,18 @@ public JsonComparisonAssert(final Path actual, final ObjectMapper mapper) { * @return this assertion object */ public JsonComparisonAssert hasSameFieldsAs(final Path expectedFile) { - return hasSameFieldsAs(readJson(expectedFile)); + return hasSameFieldsAs(JsonNodeLoader.toNode(expectedFile, mapper)); + } + + /** + * Verifies that the actual JSON file and expected JSON file contain the same field names. + * + * @param expectedFile the expected JSON file + * @return this assertion object + * @since 0.1.3 + */ + public JsonComparisonAssert hasSameFieldsAs(final File expectedFile) { + return hasSameFieldsAs(JsonNodeLoader.toNode(expectedFile, mapper)); } /** @@ -79,15 +108,20 @@ public JsonComparisonAssert hasSameFieldsAs(final Path expectedFile) { * @return this assertion object */ public JsonComparisonAssert hasSameFieldsAs(final String expectedJson) { - return hasSameFieldsAs(parseJson(expectedJson)); + return hasSameFieldsAs(JsonNodeLoader.toNode(expectedJson, mapper)); } - private JsonComparisonAssert hasSameFieldsAs(final JsonNode expected) { - JsonNode actualNode = actualJson(); - if (!sameFields(actualNode, expected)) { - failWithActualExpectedAndMessage( - actualNode, expected, "Expected JSON files to have the same fields"); - } + /** + * Verifies that the actual JSON file has the same parsed content as the expected JSON node. + * Formatting and object field ordering are ignored. + * + * @param expected the expected JSON node + * @return this assertion object + * @since 0.1.3 + */ + public JsonComparisonAssert hasSameContentAs(final JsonNode expected) { + final JsonNode actualNode = actualJson(); + assertThat(actualNode).as("JSON content from %s", actual).isEqualTo(expected); return this; } @@ -99,7 +133,18 @@ private JsonComparisonAssert hasSameFieldsAs(final JsonNode expected) { * @return this assertion object */ public JsonComparisonAssert hasSameContentAs(final Path expectedFile) { - return hasSameContentAs(readJson(expectedFile)); + return hasSameContentAs(JsonNodeLoader.toNode(expectedFile, mapper)); + } + + /** + * Verifies that the actual JSON file and expected JSON file have the same parsed content. + * + * @param expectedFile the expected JSON file + * @return this assertion object + * @since 0.1.3 + */ + public JsonComparisonAssert hasSameContentAs(final File expectedFile) { + return hasSameContentAs(JsonNodeLoader.toNode(expectedFile, mapper)); } /** @@ -109,26 +154,20 @@ public JsonComparisonAssert hasSameContentAs(final Path expectedFile) { * @return this assertion object */ public JsonComparisonAssert hasSameContentAs(final String expectedJson) { - return hasSameContentAs(parseJson(expectedJson)); - } - - private JsonComparisonAssert hasSameContentAs(final JsonNode expected) { - JsonNode actualNode = actualJson(); - assertThat(actualNode).as("JSON content from %s", actual).isEqualTo(expected); - return this; + return hasSameContentAs(JsonNodeLoader.toNode(expectedJson, mapper)); } /** - * Verifies that the JSON document in the actual file contains the supplied JSON fragment. - * Object fragments may omit fields; nested fragments are checked recursively. Arrays are - * matched by position for the elements supplied in the fragment. + * Verifies that the JSON document in the actual file contains the supplied JSON node. Object + * fragments may omit fields; nested fragments are checked recursively. Arrays are matched by + * position for the elements supplied in the fragment. * - * @param expectedFragment the JSON fragment to find + * @param expectedNode the JSON node to find * @return this assertion object + * @since 0.1.3 */ - public JsonComparisonAssert partiallyContains(final String expectedFragment) { - JsonNode actualNode = actualJson(); - JsonNode expectedNode = parseJson(expectedFragment); + public JsonComparisonAssert partiallyContains(final JsonNode expectedNode) { + final JsonNode actualNode = actualJson(); if (!contains(actualNode, expectedNode)) { failWithActualExpectedAndMessage( actualNode, expectedNode, "Expected JSON to partially contain"); @@ -136,6 +175,39 @@ public JsonComparisonAssert partiallyContains(final String expectedFragment) { return this; } + /** + * Verifies that the JSON document in the actual file contains the supplied JSON fragment + * string. + * + * @param expectedFragment the JSON fragment to find + * @return this assertion object + */ + public JsonComparisonAssert partiallyContains(final String expectedFragment) { + return partiallyContains(JsonNodeLoader.toNode(expectedFragment, mapper)); + } + + /** + * Verifies that the JSON document in the actual file contains the supplied JSON file content. + * + * @param expectedFile the JSON file to find + * @return this assertion object + * @since 0.1.3 + */ + public JsonComparisonAssert partiallyContains(final Path expectedFile) { + return partiallyContains(JsonNodeLoader.toNode(expectedFile, mapper)); + } + + /** + * Verifies that the JSON document in the actual file contains the supplied JSON file content. + * + * @param expectedFile the JSON file to find + * @return this assertion object + * @since 0.1.3 + */ + public JsonComparisonAssert partiallyContains(final File expectedFile) { + return partiallyContains(JsonNodeLoader.toNode(expectedFile, mapper)); + } + private boolean sameFields(final JsonNode actualNode, final JsonNode expectedNode) { if (actualNode.isObject() && expectedNode.isObject()) { if (actualNode.size() != expectedNode.size()) { diff --git a/src/main/java/com/archyoshi/assertj/json/api/JsonIterableAssert.java b/src/main/java/com/archyoshi/assertj/json/api/JsonIterableAssert.java index 4c7e5a7..2975262 100644 --- a/src/main/java/com/archyoshi/assertj/json/api/JsonIterableAssert.java +++ b/src/main/java/com/archyoshi/assertj/json/api/JsonIterableAssert.java @@ -16,6 +16,9 @@ package com.archyoshi.assertj.json.api; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -68,6 +71,79 @@ public static JsonIterableAssert assertThat(final Collection actual) { return new JsonIterableAssert(new ArrayList<>(actual)); } + private static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper(); + + /** + * Creates assertions for a JSON array string using the default object mapper. + * + * @param jsonArray the JSON array string + * @return an assertion object for its elements + * @since 0.1.3 + */ + public static JsonIterableAssert assertThat(final String jsonArray) { + return assertThat(jsonArray, DEFAULT_MAPPER); + } + + /** + * Creates assertions for a JSON array string using the supplied object mapper. + * + * @param jsonArray the JSON array string + * @param mapper the mapper used to parse the string + * @return an assertion object for its elements + * @since 0.1.3 + */ + public static JsonIterableAssert assertThat(final String jsonArray, final ObjectMapper mapper) { + return assertThat(JsonNodeLoader.toNode(jsonArray, mapper)); + } + + /** + * Creates assertions for a JSON array file at the given path using the default object mapper. + * + * @param jsonArrayPath the path to the JSON array file + * @return an assertion object for its elements + * @since 0.1.3 + */ + public static JsonIterableAssert assertThat(final Path jsonArrayPath) { + return assertThat(jsonArrayPath, DEFAULT_MAPPER); + } + + /** + * Creates assertions for a JSON array file at the given path using the supplied mapper. + * + * @param jsonArrayPath the path to the JSON array file + * @param mapper the mapper used to parse JSON + * @return an assertion object for its elements + * @since 0.1.3 + */ + public static JsonIterableAssert assertThat( + final Path jsonArrayPath, final ObjectMapper mapper) { + return assertThat(JsonNodeLoader.toNode(jsonArrayPath, mapper)); + } + + /** + * Creates assertions for a JSON array file using the default object mapper. + * + * @param jsonArrayFile the JSON array file + * @return an assertion object for its elements + * @since 0.1.3 + */ + public static JsonIterableAssert assertThat(final File jsonArrayFile) { + return assertThat(jsonArrayFile, DEFAULT_MAPPER); + } + + /** + * Creates assertions for a JSON array file using the supplied mapper. + * + * @param jsonArrayFile the JSON array file + * @param mapper the mapper used to parse JSON + * @return an assertion object for its elements + * @since 0.1.3 + */ + public static JsonIterableAssert assertThat( + final File jsonArrayFile, final ObjectMapper mapper) { + return assertThat(JsonNodeLoader.toNode(jsonArrayFile, mapper)); + } + /** * Verifies that at least one object element has the given field. * diff --git a/src/main/java/com/archyoshi/assertj/json/api/JsonNodeAssert.java b/src/main/java/com/archyoshi/assertj/json/api/JsonNodeAssert.java index b6d2db8..452a2e3 100644 --- a/src/main/java/com/archyoshi/assertj/json/api/JsonNodeAssert.java +++ b/src/main/java/com/archyoshi/assertj/json/api/JsonNodeAssert.java @@ -21,11 +21,22 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeType; +import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.Predicate; import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.AbstractBigDecimalAssert; +import org.assertj.core.api.AbstractBooleanAssert; +import org.assertj.core.api.AbstractDoubleAssert; +import org.assertj.core.api.AbstractIntegerAssert; +import org.assertj.core.api.AbstractLongAssert; +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.Assertions; import org.assertj.core.api.ThrowingConsumer; /** @@ -134,7 +145,10 @@ public JsonNodeAssert hasTypeForField(final String fieldName, final JsonNodeType * @param the target Java type * @return {@code this} assertion object * @since 0.1.0 + * @deprecated Use type-specific extraction methods like {@link #extractFieldAsString(String)} + * instead. */ + @Deprecated public JsonNodeAssert hasTypedValueForField( final T expectedValue, final String fieldName, final Class valueType) { final JsonNode node = actual; @@ -165,7 +179,9 @@ public JsonNodeAssert hasTypedValueForField( * @throws AssertionError if the actual JSON object is null * @throws AssertionError if the field does not exist or has a different value * @since 0.1.0 + * @deprecated Use {@link #extractFieldAsString(String)} instead. */ + @Deprecated public JsonNodeAssert hasValueForField(final String expectedValue, final String fieldName) { final JsonNode node = actual; hasField(fieldName); @@ -196,7 +212,9 @@ public JsonNodeAssert hasValueForField(final String expectedValue, final String * @throws AssertionError if the actual JSON object is null * @throws AssertionError if the field does not exist or has a different value * @since 0.1.0 + * @deprecated Use {@link #extractFieldAsInteger(String)} instead. */ + @Deprecated public JsonNodeAssert hasValueForField(final int expectedValue, final String fieldName) { final JsonNode node = actual; hasField(fieldName); @@ -212,7 +230,9 @@ public JsonNodeAssert hasValueForField(final int expectedValue, final String fie * @param fieldName the field name to verify * @return {@code this} assertion object * @since 0.1.0 + * @deprecated Use {@link #extractFieldAsInteger(String)} instead. */ + @Deprecated public JsonNodeAssert hasValueEqualForField(final int expectedValue, final String fieldName) { return hasNumericValueForField( expectedValue, fieldName, value -> value == expectedValue, "equal to"); @@ -225,7 +245,9 @@ public JsonNodeAssert hasValueEqualForField(final int expectedValue, final Strin * @param fieldName the field name to verify * @return {@code this} assertion object * @since 0.1.0 + * @deprecated Use {@link #extractFieldAsInteger(String)} instead. */ + @Deprecated public JsonNodeAssert hasValueMoreThanForField( final int expectedValue, final String fieldName) { return hasNumericValueForField( @@ -239,7 +261,9 @@ public JsonNodeAssert hasValueMoreThanForField( * @param fieldName the field name to verify * @return {@code this} assertion object * @since 0.1.0 + * @deprecated Use {@link #extractFieldAsInteger(String)} instead. */ + @Deprecated public JsonNodeAssert hasValueLessThanForField( final int expectedValue, final String fieldName) { return hasNumericValueForField( @@ -267,6 +291,20 @@ private JsonNodeAssert hasNumericValueForField( return this; } + /** + * Verifies that the actual JSON content is equal to the expected JSON node. + * + * @param expectedNode the expected JSON node + * @return {@code this} assertion object + * @throws AssertionError if the actual JSON is not equal to the expected JSON + * @since 0.1.3 + */ + public JsonNodeAssert hasJsonContent(final JsonNode expectedNode) { + final JsonNode actualNode = actual; + assertThat(actualNode).isEqualTo(expectedNode); + return this; + } + /** * Verifies that the actual JSON content is equal to the expected JSON string. * @@ -291,10 +329,31 @@ private JsonNodeAssert hasNumericValueForField( * @since 0.1.0 */ public JsonNodeAssert hasJsonContent(final String expectedJson) { - final JsonNode actualNode = actual; - final JsonNode expectedNode = parseJson(expectedJson); - assertThat(actualNode).isEqualTo(expectedNode); - return this; + return hasJsonContent(JsonNodeLoader.toNode(expectedJson, mapper)); + } + + /** + * Verifies that the actual JSON content is equal to the JSON content in the specified path. + * + * @param expectedPath the path to the expected JSON file + * @return {@code this} assertion object + * @throws AssertionError if the file cannot be read or is not equal to the expected JSON + * @since 0.1.3 + */ + public JsonNodeAssert hasJsonContent(final Path expectedPath) { + return hasJsonContent(JsonNodeLoader.toNode(expectedPath, mapper)); + } + + /** + * Verifies that the actual JSON content is equal to the JSON content in the specified file. + * + * @param expectedFile the expected JSON file + * @return {@code this} assertion object + * @throws AssertionError if the file cannot be read or is not equal to the expected JSON + * @since 0.1.3 + */ + public JsonNodeAssert hasJsonContent(final File expectedFile) { + return hasJsonContent(JsonNodeLoader.toNode(expectedFile, mapper)); } /** @@ -303,6 +362,7 @@ public JsonNodeAssert hasJsonContent(final String expectedJson) { * @param expected the expected JSON node * @param ignoredFields field names to ignore at every object level * @return {@code this} assertion object + * @since 0.1.0 */ public JsonNodeAssert isEqualToIgnoringFields( final JsonNode expected, final List ignoredFields) { @@ -315,6 +375,111 @@ public JsonNodeAssert isEqualToIgnoringFields( return this; } + /** + * Verifies structural JSON equality while ignoring named fields recursively. + * + * @param expected the expected JSON node + * @param ignoredFields field names to ignore at every object level + * @return {@code this} assertion object + * @since 0.1.3 + */ + public JsonNodeAssert isEqualToIgnoringFields( + final JsonNode expected, final String... ignoredFields) { + return isEqualToIgnoringFields( + expected, + ignoredFields != null ? Arrays.asList(ignoredFields) : Collections.emptyList()); + } + + /** + * Verifies structural JSON equality with an expected JSON string while ignoring named fields + * recursively. + * + * @param expectedJson the expected JSON string + * @param ignoredFields field names to ignore at every object level + * @return {@code this} assertion object + * @since 0.1.3 + */ + public JsonNodeAssert isEqualToIgnoringFields( + final String expectedJson, final List ignoredFields) { + return isEqualToIgnoringFields(JsonNodeLoader.toNode(expectedJson, mapper), ignoredFields); + } + + /** + * Verifies structural JSON equality with an expected JSON string while ignoring named fields + * recursively. + * + * @param expectedJson the expected JSON string + * @param ignoredFields field names to ignore at every object level + * @return {@code this} assertion object + * @since 0.1.3 + */ + public JsonNodeAssert isEqualToIgnoringFields( + final String expectedJson, final String... ignoredFields) { + return isEqualToIgnoringFields( + expectedJson, + ignoredFields != null ? Arrays.asList(ignoredFields) : Collections.emptyList()); + } + + /** + * Verifies structural JSON equality with an expected JSON path while ignoring named fields + * recursively. + * + * @param expectedPath the path to the expected JSON file + * @param ignoredFields field names to ignore at every object level + * @return {@code this} assertion object + * @since 0.1.3 + */ + public JsonNodeAssert isEqualToIgnoringFields( + final Path expectedPath, final List ignoredFields) { + return isEqualToIgnoringFields(JsonNodeLoader.toNode(expectedPath, mapper), ignoredFields); + } + + /** + * Verifies structural JSON equality with an expected JSON path while ignoring named fields + * recursively. + * + * @param expectedPath the path to the expected JSON file + * @param ignoredFields field names to ignore at every object level + * @return {@code this} assertion object + * @since 0.1.3 + */ + public JsonNodeAssert isEqualToIgnoringFields( + final Path expectedPath, final String... ignoredFields) { + return isEqualToIgnoringFields( + expectedPath, + ignoredFields != null ? Arrays.asList(ignoredFields) : Collections.emptyList()); + } + + /** + * Verifies structural JSON equality with an expected JSON file while ignoring named fields + * recursively. + * + * @param expectedFile the expected JSON file + * @param ignoredFields field names to ignore at every object level + * @return {@code this} assertion object + * @since 0.1.3 + */ + public JsonNodeAssert isEqualToIgnoringFields( + final File expectedFile, final List ignoredFields) { + return isEqualToIgnoringFields(JsonNodeLoader.toNode(expectedFile, mapper), ignoredFields); + } + + /** + * Verifies structural JSON equality with an expected JSON file while ignoring named fields + * recursively. + * + * @param expectedFile the expected JSON file + * @param ignoredFields field names to ignore at every object level + * @return {@code this} assertion object + * @since 0.1.3 + */ + public JsonNodeAssert isEqualToIgnoringFields( + final File expectedFile, final String... ignoredFields) { + return isEqualToIgnoringFields( + expectedFile, + ignoredFields != null ? Arrays.asList(ignoredFields) : Collections.emptyList()); + } + private boolean equalsIgnoringFields( final JsonNode actualNode, final JsonNode expectedNode, @@ -528,6 +693,114 @@ public JsonNodeAssert hasSizeForArrayField(final int expectedSize, final String return this; } + /** + * Extracts a child field as a String for standard AssertJ string assertions. + * + * @param fieldName the field name to extract + * @return a String assertion object + * @since 0.1.3 + */ + public AbstractStringAssert extractFieldAsString(final String fieldName) { + hasField(fieldName); + final JsonNode field = actual.get(fieldName); + if (!field.isTextual()) { + failWithMessage( + "Expected field '%s' to be a STRING but was <%s>", + fieldName, field.getNodeType()); + } + return Assertions.assertThat(field.asText()); + } + + /** + * Extracts a child field as an Integer for standard AssertJ integer assertions. + * + * @param fieldName the field name to extract + * @return an Integer assertion object + * @since 0.1.3 + */ + public AbstractIntegerAssert extractFieldAsInteger(final String fieldName) { + hasField(fieldName); + final JsonNode field = actual.get(fieldName); + if (!field.isInt() && !field.canConvertToInt()) { + failWithMessage( + "Expected field '%s' to be an INTEGER but was <%s>", + fieldName, field.getNodeType()); + } + return Assertions.assertThat(field.asInt()); + } + + /** + * Extracts a child field as a Long for standard AssertJ long assertions. + * + * @param fieldName the field name to extract + * @return a Long assertion object + * @since 0.1.3 + */ + public AbstractLongAssert extractFieldAsLong(final String fieldName) { + hasField(fieldName); + final JsonNode field = actual.get(fieldName); + if (!field.isLong() && !field.canConvertToLong()) { + failWithMessage( + "Expected field '%s' to be a LONG but was <%s>", + fieldName, field.getNodeType()); + } + return Assertions.assertThat(field.asLong()); + } + + /** + * Extracts a child field as a Double for standard AssertJ double assertions. + * + * @param fieldName the field name to extract + * @return a Double assertion object + * @since 0.1.3 + */ + public AbstractDoubleAssert extractFieldAsDouble(final String fieldName) { + hasField(fieldName); + final JsonNode field = actual.get(fieldName); + if (!field.isDouble() && !field.isNumber()) { + failWithMessage( + "Expected field '%s' to be a DOUBLE but was <%s>", + fieldName, field.getNodeType()); + } + return Assertions.assertThat(field.asDouble()); + } + + /** + * Extracts a child field as a Boolean for standard AssertJ boolean assertions. + * + * @param fieldName the field name to extract + * @return a Boolean assertion object + * @since 0.1.3 + */ + public AbstractBooleanAssert extractFieldAsBoolean(final String fieldName) { + hasField(fieldName); + final JsonNode field = actual.get(fieldName); + if (!field.isBoolean()) { + failWithMessage( + "Expected field '%s' to be a BOOLEAN but was <%s>", + fieldName, field.getNodeType()); + } + return Assertions.assertThat(field.asBoolean()); + } + + /** + * Extracts a child field as a BigDecimal for standard AssertJ BigDecimal assertions. + * + * @param fieldName the field name to extract + * @return a BigDecimal assertion object + * @since 0.1.3 + */ + public AbstractBigDecimalAssert extractFieldAsBigDecimal(final String fieldName) { + hasField(fieldName); + final JsonNode field = actual.get(fieldName); + if (!field.isNumber()) { + failWithMessage( + "Expected field '%s' to be a NUMBER but was <%s>", + fieldName, field.getNodeType()); + } + return Assertions.assertThat(field.decimalValue()); + } + /** * Extracts a child field for further JSON assertions. * diff --git a/src/main/java/com/archyoshi/assertj/json/api/JsonNodeLoader.java b/src/main/java/com/archyoshi/assertj/json/api/JsonNodeLoader.java new file mode 100644 index 0000000..cba59e2 --- /dev/null +++ b/src/main/java/com/archyoshi/assertj/json/api/JsonNodeLoader.java @@ -0,0 +1,101 @@ +/* + * Copyright (C)2026 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 com.archyoshi.assertj.json.api; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Adapter utility for converting various JSON sources (strings, files, paths) into {@link JsonNode} + * instances. + * + * @author archyoshi + * @since 0.1.3 + */ +public final class JsonNodeLoader { + + private JsonNodeLoader() { + throw new UnsupportedOperationException("Utility class should not be instantiated"); + } + + /** + * Returns the given {@link JsonNode} or throws an {@link AssertionError} if it is null. + * + * @param node the JSON node + * @return the same JSON node + */ + public static JsonNode toNode(final JsonNode node) { + if (node == null) { + throw new AssertionError("Expected JSON node not to be null"); + } + return node; + } + + /** + * Parses a JSON string into a {@link JsonNode} using the given mapper. + * + * @param json the JSON string + * @param mapper the mapper to use + * @return the parsed JSON node + */ + public static JsonNode toNode(final String json, final ObjectMapper mapper) { + if (json == null) { + throw new AssertionError("Expected JSON string not to be null"); + } + try { + return mapper.readTree(json); + } catch (final JsonProcessingException e) { + throw new AssertionError("Invalid JSON content: " + e.getOriginalMessage(), e); + } + } + + /** + * Reads and parses a JSON file from a {@link Path} using the given mapper. + * + * @param path the path to the JSON file + * @param mapper the mapper to use + * @return the parsed JSON node + */ + public static JsonNode toNode(final Path path, final ObjectMapper mapper) { + if (path == null) { + throw new AssertionError("Expected JSON path not to be null"); + } + try { + return toNode(Files.readString(path), mapper); + } catch (final IOException e) { + throw new AssertionError("Unable to read JSON file: " + path, e); + } + } + + /** + * Reads and parses a JSON file from a {@link File} using the given mapper. + * + * @param file the JSON file + * @param mapper the mapper to use + * @return the parsed JSON node + */ + public static JsonNode toNode(final File file, final ObjectMapper mapper) { + if (file == null) { + throw new AssertionError("Expected JSON file not to be null"); + } + return toNode(file.toPath(), mapper); + } +} diff --git a/src/test/java/com/archyoshi/assertj/json/JsonAssertionsTest.java b/src/test/java/com/archyoshi/assertj/json/JsonAssertionsTest.java index 606d11e..f3f2c0c 100644 --- a/src/test/java/com/archyoshi/assertj/json/JsonAssertionsTest.java +++ b/src/test/java/com/archyoshi/assertj/json/JsonAssertionsTest.java @@ -16,9 +16,11 @@ package com.archyoshi.assertj.json; import static com.archyoshi.assertj.json.JsonAssertions.assertThat; +import static com.archyoshi.assertj.json.JsonAssertions.assertThatArray; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.api.Test; @@ -78,4 +80,33 @@ void shouldUseProvidedObjectMapperForFileParsing() throws Exception { assertThat(actual, mapper).hasSameContentAs(expected); } + + @Test + void shouldAssertOnFileDirectly() throws Exception { + final Path actual = Files.createTempFile("assertj-json-", ".json"); + Files.writeString(actual, "{\"name\":\"Vegeta\"}"); + final File file = actual.toFile(); + + assertThat(file).hasSameFieldsAs("{\"name\":\"Goku\"}"); + } + + @Test + void shouldAssertOnJsonArrayFromStringPathAndFile() throws Exception { + final String jsonArray = "[{\"name\":\"Vegeta\"},{\"name\":\"Goku\"}]"; + final Path arrayPath = Files.createTempFile("assertj-json-arr-", ".json"); + Files.writeString(arrayPath, jsonArray); + final File arrayFile = arrayPath.toFile(); + + assertThatArray(jsonArray) + .containsElementWithFieldName("name") + .containsElementWithFieldAndValue("name", "Vegeta"); + + assertThatArray(arrayPath) + .containsElementWithFieldName("name") + .containsElementWithFieldAndValue("name", "Goku"); + + assertThatArray(arrayFile) + .containsElementWithFieldName("name") + .containsElementWithFieldAndValue("name", "Vegeta"); + } } diff --git a/src/test/java/com/archyoshi/assertj/json/api/JsonComparisonAssertTest.java b/src/test/java/com/archyoshi/assertj/json/api/JsonComparisonAssertTest.java index e3f6d7a..d84ff1d 100644 --- a/src/test/java/com/archyoshi/assertj/json/api/JsonComparisonAssertTest.java +++ b/src/test/java/com/archyoshi/assertj/json/api/JsonComparisonAssertTest.java @@ -18,6 +18,9 @@ import static com.archyoshi.assertj.json.JsonAssertions.assertThat; import static org.assertj.core.api.BDDAssertions.thenThrownBy; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.api.Test; @@ -72,6 +75,23 @@ void shouldMatchPartialObjectAndArrayContent() throws Exception { .partiallyContains("{\"profile\":{\"planet\":\"Vegeta\"},\"items\":[{\"id\":1}]}"); } + @Test + void shouldSupportJsonNodeAndFileForComparisons() throws Exception { + Path actual = jsonFile("{\"name\":\"Vegeta\",\"age\":30}"); + Path expectedPath = jsonFile("{\"age\":30,\"name\":\"Vegeta\"}"); + File expectedFile = expectedPath.toFile(); + JsonNode expectedNode = new ObjectMapper().readTree("{\"age\":30,\"name\":\"Vegeta\"}"); + + assertThat(actual) + .hasSameFieldsAs(expectedNode) + .hasSameFieldsAs(expectedFile) + .hasSameContentAs(expectedNode) + .hasSameContentAs(expectedFile) + .partiallyContains(expectedNode) + .partiallyContains(expectedPath) + .partiallyContains(expectedFile); + } + @Test void shouldFailWhenPartialContentIsMissing() throws Exception { Path actual = jsonFile("{\"name\":\"Vegeta\"}"); diff --git a/src/test/java/com/archyoshi/assertj/json/api/JsonIterableAssertTest.java b/src/test/java/com/archyoshi/assertj/json/api/JsonIterableAssertTest.java index 1bf63cf..62eac6f 100644 --- a/src/test/java/com/archyoshi/assertj/json/api/JsonIterableAssertTest.java +++ b/src/test/java/com/archyoshi/assertj/json/api/JsonIterableAssertTest.java @@ -92,7 +92,7 @@ void shouldFailWhenExtractingUnknownElement() { @Test void shouldRejectNullAndNonArrayNodes() throws Exception { - thenThrownBy(() -> assertThatArray(null)).isInstanceOf(AssertionError.class); + thenThrownBy(() -> assertThatArray((JsonNode) null)).isInstanceOf(AssertionError.class); thenThrownBy(() -> assertThatArray(mapper.readTree("{}"))) .isInstanceOf(AssertionError.class); } diff --git a/src/test/java/com/archyoshi/assertj/json/api/JsonNodeAssertAdditionalTest.java b/src/test/java/com/archyoshi/assertj/json/api/JsonNodeAssertAdditionalTest.java index 720db12..70b1e96 100644 --- a/src/test/java/com/archyoshi/assertj/json/api/JsonNodeAssertAdditionalTest.java +++ b/src/test/java/com/archyoshi/assertj/json/api/JsonNodeAssertAdditionalTest.java @@ -22,9 +22,13 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeType; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; /** * @author archyoshi @@ -39,7 +43,7 @@ void setUp() throws JsonProcessingException { new ObjectMapper() .readTree( """ - {"name":"Vegeta","age":30,"active":true,\ + {"name":"Vegeta","age":30,"active":true,"score":9.5,"power":9000000000,\ "profile":{"planet":"Vegeta"},"items":[{"id":1},{"id":2}]}\ """); } @@ -79,21 +83,71 @@ void shouldFailForInvalidFieldAssertions() { .isInstanceOf(AssertionError.class); } + @TempDir Path tempDir; + @Test void shouldCompareWhileIgnoringNestedFields() throws JsonProcessingException { final JsonNode expected = new ObjectMapper() .readTree( """ - {"name":"Vegeta","age":30,"active":false,\ + {"name":"Vegeta","age":30,"active":false,"score":9.5,"power":9000000000,\ "profile":{"planet":"Earth"},"items":[{"id":1},{"id":2}]}\ """); assertThat(actual).isEqualToIgnoringFields(expected, List.of("active", "planet")); + assertThat(actual).isEqualToIgnoringFields(expected, "active", "planet"); thenThrownBy(() -> assertThat(actual).isEqualToIgnoringFields(expected, List.of("active"))) .isInstanceOf(AssertionError.class); } + @Test + void shouldCompareWhileIgnoringNestedFieldsUsingString() { + final String expectedJson = + """ + {"name":"Vegeta","age":30,"active":false,"score":9.5,"power":9000000000,\ + "profile":{"planet":"Earth"},"items":[{"id":1},{"id":2}]}\ + """; + + assertThat(actual).isEqualToIgnoringFields(expectedJson, List.of("active", "planet")); + assertThat(actual).isEqualToIgnoringFields(expectedJson, "active", "planet"); + thenThrownBy(() -> assertThat(actual).isEqualToIgnoringFields(expectedJson, "active")) + .isInstanceOf(AssertionError.class); + } + + @Test + void shouldCompareWhileIgnoringNestedFieldsUsingPathAndFile() throws Exception { + final String expectedJson = + """ + {"name":"Vegeta","age":30,"active":false,"score":9.5,"power":9000000000,\ + "profile":{"planet":"Earth"},"items":[{"id":1},{"id":2}]}\ + """; + final Path path = + Files.writeString( + Files.createTempFile(tempDir, "expected-", ".json"), expectedJson); + final File file = path.toFile(); + + assertThat(actual).isEqualToIgnoringFields(path, List.of("active", "planet")); + assertThat(actual).isEqualToIgnoringFields(path, "active", "planet"); + assertThat(actual).isEqualToIgnoringFields(file, List.of("active", "planet")); + assertThat(actual).isEqualToIgnoringFields(file, "active", "planet"); + } + + @Test + void shouldAssertHasJsonContentWithNodePathAndFile() throws Exception { + final String json = + "{\"name\":\"Vegeta\",\"age\":30,\"active\":true,\"score\":9.5,\"power\":9000000000,\"profile\":{\"planet\":\"Vegeta\"},\"items\":[{\"id\":1},{\"id\":2}]}"; + final JsonNode expectedNode = new ObjectMapper().readTree(json); + final Path expectedPath = + Files.writeString(Files.createTempFile(tempDir, "content-", ".json"), json); + final File expectedFile = expectedPath.toFile(); + + assertThat(actual).hasJsonContent(expectedNode); + assertThat(actual).hasJsonContent(json); + assertThat(actual).hasJsonContent(expectedPath); + assertThat(actual).hasJsonContent(expectedFile); + } + @Test void shouldExtractNestedNodesAndArrays() { assertThat(actual) @@ -113,4 +167,44 @@ void shouldApplyCustomNodeAssertions() { } }); } + + @Test + void shouldExtractFieldAsString() { + assertThat(actual).extractFieldAsString("name").startsWith("Veg").endsWith("eta"); + + thenThrownBy(() -> assertThat(actual).extractFieldAsString("age")) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected field 'age' to be a STRING"); + } + + @Test + void shouldExtractFieldAsInteger() { + assertThat(actual).extractFieldAsInteger("age").isGreaterThan(20).isLessThan(40); + + thenThrownBy(() -> assertThat(actual).extractFieldAsInteger("name")) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected field 'name' to be an INTEGER"); + } + + @Test + void shouldExtractFieldAsLong() { + assertThat(actual).extractFieldAsLong("power").isGreaterThan(8000000000L); + } + + @Test + void shouldExtractFieldAsDouble() { + assertThat(actual).extractFieldAsDouble("score").isBetween(9.0, 10.0); + } + + @Test + void shouldExtractFieldAsBoolean() { + assertThat(actual).extractFieldAsBoolean("active").isTrue(); + } + + @Test + void shouldExtractFieldAsBigDecimal() { + assertThat(actual) + .extractFieldAsBigDecimal("score") + .isGreaterThan(java.math.BigDecimal.valueOf(9.0)); + } }