Skip to content

feat(assertion): Add IsActualEqualToExpectedJsonValueComparer for JSON columns - #922

Merged
jeffjensen merged 1 commit into
mainfrom
921-json-value-comparer
Aug 9, 2026
Merged

feat(assertion): Add IsActualEqualToExpectedJsonValueComparer for JSON columns#922
jeffjensen merged 1 commit into
mainfrom
921-json-value-comparer

Conversation

@jeffjensen

@jeffjensen jeffjensen commented Aug 9, 2026

Copy link
Copy Markdown
Member

Summary

  • Reviewed a Stack Overflow report of DbUnit failing to compare a MySQL JSON column (https://stackoverflow.com/a/55839637/2848514). MySQL Connector/J already reports native JSON columns as Types.LONGVARCHAR, which DbUnit's existing StringDataType already handles for reads/writes - no DataTypeFactory change needed there, unlike the SO thread's own Postgres/H2 answers.
  • The actual gap: MySQL (like PostgreSQL jsonb and H2 JSON) reformats JSON text on storage - sorting object keys, stripping insignificant whitespace - so a literal string/value equality comparison against an expected dataset spuriously fails even when the JSON is semantically identical.
  • Add IsActualEqualToExpectedJsonValueComparer (org.dbunit.assertion.comparer.value): parses both sides with Jackson and compares the resulting document trees - object member order ignored, array element order still significant, matching JSON's own equality semantics. Database-agnostic by design (works for MySQL/PostgreSQL/H2/MariaDB JSON columns), using the existing ValueComparer extension point rather than a per-database DataType workaround.
  • Deliberately not exposed as a ValueComparers constant, since that class eagerly instantiates every constant it declares, which would force the optional jackson-databind dependency onto every consumer, not only those comparing JSON columns.
  • Document the new comparer in valuecomparer.adoc, explaining why it's absent from ValueComparers.

Refs: 921

Test plan

  • New unit test IsActualEqualToExpectedJsonValueComparerTest (13 cases: null handling, identical text, whitespace-only/key-order differences, equivalent nested objects/arrays, array-order sensitivity, differing values, malformed JSON on either side)
  • Full unit suite green (./mvnw clean test, 2048 tests, 0 failures)
  • ./mvnw javadoc:javadoc clean (no doclint warnings)

Summary by Sourcery

Add a JSON-aware value comparer for assertion of JSON column values and document its behavior and rationale, backed by unit tests and a changelog entry.

New Features:

  • Introduce IsActualEqualToExpectedJsonValueComparer to compare expected and actual column values by parsing them as JSON and comparing document trees, ignoring object key order while preserving array element order.

Enhancements:

  • Document the JSON value comparer in the data comparison/value comparer documentation, including why it is not exposed via ValueComparers constants.

Documentation:

  • Update value comparer documentation to describe JSON semantic comparison support and its usage constraints.

Tests:

  • Add IsActualEqualToExpectedJsonValueComparerTest covering null handling, whitespace and key-order differences, nested structures, array-order sensitivity, differing values, and malformed JSON inputs.

Chores:

  • Record the addition of IsActualEqualToExpectedJsonValueComparer and its motivation in the project changelog for release 3.4.1.

Summary by CodeRabbit

  • New Features

    • Added JSON value comparison for expected and actual data.
    • JSON objects are compared regardless of member order or insignificant whitespace.
    • Array element order remains significant.
    • Supports nested JSON structures and clear handling of null or invalid JSON values.
  • Documentation

    • Added guidance for using the JSON/JSONB comparer directly when needed.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sourcery-ai

sourcery-ai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a new JSON-aware ValueComparer implementation, wires it into documentation and change log, and tests its behavior across nulls, formatting differences, structural equality, and error cases when parsing JSON.

Sequence diagram for JSON value comparison in IsActualEqualToExpectedJsonValueComparer

sequenceDiagram
    participant Test as IsActualEqualToExpectedJsonValueComparerTest
    participant Comparer as IsActualEqualToExpectedJsonValueComparer
    participant DataType
    participant ObjectMapper

    Test->>Comparer: isExpected(expectedTable, actualTable, rowNum, columnName, dataType, expectedValue, actualValue)
    alt [both values null]
        Comparer-->>Test: true
    else [one value null]
        Comparer-->>Test: false
    else [neither null]
        Comparer->>Comparer: isJsonEqual(expectedValue, actualValue)
        Comparer->>Comparer: parseJson("expected", expectedValue)
        Comparer->>DataType: asString(expectedValue)
        DataType-->>Comparer: jsonString
        Comparer->>ObjectMapper: readTree(jsonString)
        ObjectMapper-->>Comparer: expectedNode
        Comparer->>Comparer: parseJson("actual", actualValue)
        Comparer->>DataType: asString(actualValue)
        DataType-->>Comparer: jsonString
        Comparer->>ObjectMapper: readTree(jsonString)
        ObjectMapper-->>Comparer: actualNode
        Comparer-->>Test: actualNode.equals(expectedNode)
    end
Loading

File-Level Changes

Change Details Files
Introduce a JSON value comparer that uses Jackson to compare semantic equality of JSON documents instead of raw string equality, with explicit null-handling and a custom failure phrase.
  • Implement IsActualEqualToExpectedJsonValueComparer extending ValueComparerTemplateBase.
  • Handle nulls explicitly: both-null returns true, one-null returns false, non-null values are compared as JSON.
  • Use DataType.asString and a shared ObjectMapper to parse expected and actual values into JsonNode instances, logging parsed nodes at debug level.
  • Throw DatabaseUnitException with a descriptive message when either side cannot be converted to JSON.
  • Define getFailPhrase to return a JSON-specific comparison phrase.
src/main/java/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparer.java
Add unit coverage for the JSON comparer across equality, inequality, null, and malformed JSON scenarios.
  • Create IsActualEqualToExpectedJsonValueComparerTest exercising the isExpected contract over 13 scenarios (null combinations, identical text, whitespace-only differences, key-order differences, nested structures, array order sensitivity, different values, missing keys, invalid JSON on either side).
  • Verify that malformed JSON yields DatabaseUnitException via assertThatExceptionOfType.
  • Ensure getFailPhrase returns a non-null phrase.
src/test/java/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparerTest.java
Document and announce the new JSON comparer without eagerly exposing it via ValueComparers to avoid hard-requiring jackson-databind for all consumers.
  • Add a change-log entry describing the new IsActualEqualToExpectedJsonValueComparer, its JSON semantics, its database-agnostic intent, and the rationale for not exposing it via ValueComparers.
  • Extend valuecomparer.adoc to describe the comparer, its behavior, and why it is not declared as a ValueComparers constant.
src/changes/changes.xml
src/site/asciidoc/datacomparisons/valuecomparer.adoc

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jeffjensen, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2629a420-c72f-40bb-9d97-d548c7712a7f

📥 Commits

Reviewing files that changed from the base of the PR and between 18ccbd2 and da25f10.

📒 Files selected for processing (2)
  • src/main/java/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparer.java
  • src/test/java/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparerTest.java
📝 Walkthrough

Walkthrough

Adds IsActualEqualToExpectedJsonValueComparer for structural JSON comparison. Object member order is ignored, array order remains significant, parse failures become DatabaseUnitException, and direct construction is documented because the comparer is not registered globally.

Changes

JSON comparison

Layer / File(s) Summary
JSON comparer implementation
src/main/java/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparer.java
Adds null-aware JSON tree comparison with Jackson. Object member order is ignored, array order remains significant, parse failures become DatabaseUnitException, and failures use "not JSON-equal to".
Validation and usage documentation
src/test/java/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparerTest.java, src/site/asciidoc/datacomparisons/valuecomparer.adoc, src/changes/changes.xml
Adds coverage for equality, inequality, invalid JSON, and failure messages. Documents direct construction and records the release change.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • dbunit/dbunit-extension issue 921: The pull request implements the JSON-semantic ValueComparer described by this issue.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the new JSON value comparer added for assertion support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 921-json-value-comparer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • Consider making the Logger field private static final instead of an instance field to avoid redundant logger creation per comparer instance.
  • The parseJson error message only differentiates expected vs actual; consider including column/row context (e.g., columnName, rowNum) in the DatabaseUnitException to make JSON parse failures easier to diagnose during assertions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider making the `Logger` field `private static final` instead of an instance field to avoid redundant logger creation per comparer instance.
- The `parseJson` error message only differentiates `expected` vs `actual`; consider including column/row context (e.g., `columnName`, `rowNum`) in the `DatabaseUnitException` to make JSON parse failures easier to diagnose during assertions.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparer.java`:
- Around line 97-104: Update the JSON parsing logic in the visible comparer
method to store the result of ObjectMapper.readTree, reject a null node by
throwing DatabaseUnitException for empty or whitespace-only input, and preserve
the existing IOException handling. Add tests covering both empty and
whitespace-only expected and actual values.

In
`@src/test/java/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparerTest.java`:
- Line 187: Rename testGetFailPhrase_returnsNonNullPhrase to include the default
comparer starting state, following the
test<MethodName>_<StartingStateConditions>_<AssertedOutcome> convention while
preserving the existing asserted outcome.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d42d6f8-33ce-4f8d-926e-2e6c7481717e

📥 Commits

Reviewing files that changed from the base of the PR and between 8354693 and 18ccbd2.

📒 Files selected for processing (4)
  • src/changes/changes.xml
  • src/main/java/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparer.java
  • src/site/asciidoc/datacomparisons/valuecomparer.adoc
  • src/test/java/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparerTest.java

@jeffjensen jeffjensen linked an issue Aug 9, 2026 that may be closed by this pull request
…N columns

Reviewed a Stack Overflow report of DbUnit failing to compare a MySQL
JSON column (https://stackoverflow.com/a/55839637/2848514). MySQL
Connector/J already reports native JSON columns as Types.LONGVARCHAR,
which DbUnit's existing StringDataType handles for reads/writes with no
DataTypeFactory change needed - but MySQL (like PostgreSQL jsonb and H2
JSON) reformats the text on storage, sorting object keys and stripping
insignificant whitespace, so a literal string comparison against an
expected dataset value spuriously fails even when the JSON is
semantically identical. The SO thread's own suggested fix (a custom
DataTypeFactory binding a driver-specific object such as PGobject) does
not apply here and targets a different, write-path problem specific to
PostgreSQL's stricter parameter binding.

* Add IsActualEqualToExpectedJsonValueComparer, parsing both sides with
  Jackson and comparing the resulting document trees: object member
  order is ignored while array element order stays significant,
  matching JSON's own equality semantics. Database-agnostic - applies
  to any column that round-trips as text, not only MySQL.
* Deliberately not exposed as a ValueComparers constant, since that
  class eagerly instantiates every constant it declares and would force
  the optional jackson-databind dependency onto every consumer, not
  only those comparing JSON columns.
* Add unit coverage: null handling, identical text, whitespace-only and
  object-key-order differences, equivalent nested objects/arrays,
  array-order sensitivity, differing values, and malformed-JSON
  failures on both sides.
* Document the new comparer in valuecomparer.adoc, noting why it is
  absent from ValueComparers and pointing readers there directly.

Refs: 921

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016E3LGxvPZgjZjK1GHrcZpJ
@jeffjensen
jeffjensen force-pushed the 921-json-value-comparer branch from 18ccbd2 to da25f10 Compare August 9, 2026 02:15
@jeffjensen

Copy link
Copy Markdown
Member Author

Replying to Sourcery's two overall comments (da25f10):

  • Row/column context in JSON parse-failure messages — applied. isJsonEqual/parseJson now take rowNum/columnName and fold them into the DatabaseUnitException message, e.g. Unable to parse actual value as JSON for column 'MY_COLUMN', row 3: not json. The two malformed-JSON tests now assert the message contains both.
  • Make the Logger field private static final — declining this one. Every sibling ValueComparer in this package (IsActualContainingExpectedStringValueComparer and others) deliberately uses an instance field initialized with getClass() rather than a literal class reference, so a subclass's log lines report its own class name instead of the base class's. SLF4J's LoggerFactory.getLogger is also already an internal cached lookup (not a fresh Logger built per call), so there's no real "redundant creation" cost being traded away — keeping this file consistent with the rest of the package outweighs the micro-optimization.

@jeffjensen
jeffjensen merged commit e5e0184 into main Aug 9, 2026
29 checks passed
@jeffjensen
jeffjensen deleted the 921-json-value-comparer branch August 9, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add JSON-semantic ValueComparer for comparing JSON/JSONB columns

1 participant