├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── com │ └── deblock │ └── jsondiff │ ├── DiffGenerator.java │ ├── diff │ ├── JsonArrayDiff.java │ ├── JsonDiff.java │ ├── JsonObjectDiff.java │ ├── MatchedPrimaryDiff.java │ └── UnMatchedPrimaryDiff.java │ ├── matcher │ ├── CompositeJsonMatcher.java │ ├── JsonMatcher.java │ ├── LenientJsonArrayPartialMatcher.java │ ├── LenientJsonObjectPartialMatcher.java │ ├── LenientNumberPrimitivePartialMatcher.java │ ├── PartialJsonMatcher.java │ ├── Path.java │ ├── StrictJsonArrayPartialMatcher.java │ ├── StrictJsonObjectPartialMatcher.java │ └── StrictPrimitivePartialMatcher.java │ └── viewer │ ├── JsonDiffViewer.java │ ├── OnlyErrorDiffViewer.java │ └── PatchDiffViewer.java └── test └── java └── com └── deblock └── jsondiff ├── DiffGeneratorTest.java ├── Sample.java ├── matcher ├── CompositeJsonMatcherTest.java ├── JsonDiffAsserter.java ├── LenientJsonArrayPartialMatcherTest.java ├── LenientJsonObjectPartialMatcherTest.java ├── LenientNumberPrimitivePartialMatcherTest.java ├── PathTest.java ├── StrictJsonArrayPartialMatcherTest.java ├── StrictJsonObjectPartialMatcherTest.java └── StrictPrimitivePartialMatcherTest.java └── viewer ├── OnlyErrorDiffViewerTest.java ├── PatchDiffViewerTest.java ├── actual.json ├── expected.json ├── lenientDiff.patch └── strictDiff.patch /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to OSS Sonatype 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | jobs: 8 | publish: 9 | 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up JDK 11 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: '11' 21 | distribution: 'temurin' 22 | server-id: github 23 | settings-path: ${{ github.workspace }} 24 | - name: Setup Gradle 25 | uses: gradle/gradle-build-action@v2 26 | 27 | - name: Build with Gradle 28 | uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 29 | with: 30 | arguments: build 31 | 32 | - name: Publish to maven central 33 | uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 34 | with: 35 | arguments: publishToSonatype closeAndReleaseSonatypeStagingRepository 36 | env: 37 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USERNAME }} 38 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }} 39 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }} 40 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | unit_test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up JDK 11 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '11' 20 | distribution: 'temurin' 21 | - name: Setup gradle 22 | uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 23 | - name: Build with Gradle 24 | uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 25 | with: 26 | arguments: build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | *.iml 4 | build 5 | .gradle -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java json-diff 2 | 3 | A customizable lib to perform a json-diff 4 | 5 | ## Why Use json-diff library 6 | 7 | The goal of this library is to provide a readable diff between two json file. 8 | 9 | In addition to the differential, a similarity score is calculated. 10 | This score can be used to compare several json with each other and find the two most similar. 11 | 12 | The way to compare json is completely customisable. 13 | 14 | 2 way to display diff are provided by default (patch file, text file). And you can easily create your own formatter. 15 | 16 | ## Installation 17 | 18 | maven: 19 | ```xml 20 | 21 | io.github.deblockt 22 | json-diff 23 | 0.1.0 24 | 25 | ``` 26 | 27 | gradle: 28 | ```gradle 29 | implementation 'io.github.deblockt:json-diff:0.1.0' 30 | ``` 31 | 32 | ## Usage 33 | 34 | example: 35 | ```java 36 | final var expectedJson = "{\"additionalProperty\":\"a\", \"foo\": \"bar\", \"bar\": \"bar\", \"numberMatch\": 10.0, \"numberUnmatched\": 10.01, \"arrayMatch\": [{\"b\":\"a\"}], \"arrayUnmatched\": [{\"b\":\"a\"}]}"; 37 | final var receivedJson = "{\"foo\": \"foo\", \"bar\": \"bar\", \"numberMatch\": 10, \"numberUnmatched\": 10.02, \"arrayMatch\": [{\"b\":\"a\"}], \"arrayUnmatched\": {\"b\":\"b\"}}"; 38 | 39 | // define your matcher 40 | // CompositeJsonMatcher use other matcher to perform matching on objects, list or primitive 41 | final var jsonMatcher = new CompositeJsonMatcher( 42 | new LenientJsonArrayPartialMatcher(), // comparing array using lenient mode (ignore array order and extra items) 43 | new LenientJsonObjectPartialMatcher(), // comparing object using lenient mode (ignoring extra properties) 44 | new LenientNumberPrimitivePartialMatcher(new StrictPrimitivePartialMatcher()) // comparing primitive types and manage numbers (100.00 == 100) 45 | ); 46 | 47 | // generate a diff 48 | final var jsondiff = DiffGenerator.diff(expectedJson, receivedJson, jsonMatcher); 49 | 50 | // use the viewer to collect diff data 51 | final var errorsResult= OnlyErrorDiffViewer.from(jsondiff); 52 | 53 | // print the diff result 54 | System.out.println(errorsResult); 55 | // print a similarity ratio between expected and received json (0 <= ratio <= 100) 56 | System.out.println(jsondiff.similarityRate()); 57 | ``` 58 | Result: 59 | ``` 60 | The property "$.additionalProperty" is not found 61 | The property "$.numberUnmatched" didn't match. Expected 10.01, Received: 10.02 62 | The property "$.arrayUnmatched" didn't match. Expected [{"b":"a"}], Received: {"b":"b"} 63 | The property "$.foo" didn't match. Expected "bar", Received: "foo" 64 | 65 | 76.0 66 | ``` 67 | 68 | You can also generate a patch file using this viewer: 69 | ```java 70 | final var patch = PatchDiffViewer.from(jsondiff); 71 | 72 | // use the viewer to collect diff data 73 | final var patchFile= PatchDiffViewer.from(jsondiff); 74 | 75 | // print the diff result 76 | System.out.println(patchFile); 77 | ``` 78 | 79 | Result: 80 | ``` diff 81 | --- actual 82 | +++ expected 83 | @@ @@ 84 | { 85 | + "additionalProperty": "a", 86 | "bar": "bar", 87 | - "numberUnmatched": 10.02, 88 | + "numberUnmatched": 10.01, 89 | - "arrayUnmatched": {"b":"b"}, 90 | + "arrayUnmatched": [{"b":"a"}], 91 | - "foo": "foo", 92 | + "foo": "bar", 93 | "numberMatch": 10.0, 94 | "arrayMatch": [ 95 | { 96 | "b": "a" 97 | } 98 | ] 99 | } 100 | ``` 101 | 102 | ### Comparison mode 103 | 104 | You can use many comparison mode to compare you json: 105 | 106 | If you want compare json using *lenient* comparison: 107 | ```java 108 | final var fullLenient = new CompositeJsonMatcher( 109 | new LenientJsonArrayPartialMatcher(), // comparing array using lenient mode (ignore array order and extra items) 110 | new LenientJsonObjectPartialMatcher(), // comparing object using lenient mode (ignoring extra properties) 111 | new LenientNumberPrimitivePartialMatcher(new StrictPrimitivePartialMatcher()) // comparing primitive types and manage numbers (100.00 == 100) 112 | ); 113 | ``` 114 | 115 | If you want compare json using *strict* comparison: 116 | ```java 117 | final var strictMatcher = new CompositeJsonMatcher( 118 | new StrictJsonArrayPartialMatcher(), // comparing array using strict mode (object should have same properties/value) 119 | new StrictJsonObjectPartialMatcher(), // comparing object using strict mode (array should have same item on same orders) 120 | new StrictPrimitivePartialMatcher() // comparing primitive types (values should be strictly equals type and value) 121 | ); 122 | ``` 123 | 124 | You can mix matcher. For example, be lenient on array and strict on object: 125 | ```java 126 | final var matcher = new CompositeJsonMatcher( 127 | new LenientJsonArrayPartialMatcher(), // comparing array using lenient mode (ignore array order and extra items) 128 | new StrictJsonObjectPartialMatcher(), // comparing object using strict mode (array should have same item on same orders) 129 | new StrictPrimitivePartialMatcher() // comparing primitive types (values should be strictly equals type and value) 130 | ); 131 | ``` 132 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * This project uses @Incubating APIs which are subject to change. 5 | */ 6 | 7 | plugins { 8 | id 'io.github.gradle-nexus.publish-plugin' version '1.3.0' 9 | id 'me.qoomon.git-versioning' version '6.4.1' 10 | id 'java' 11 | id 'maven-publish' 12 | id 'signing' 13 | } 14 | 15 | repositories { 16 | mavenLocal() 17 | maven { 18 | url = uri('https://repo.maven.apache.org/maven2/') 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' 24 | testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' 25 | testImplementation 'org.mockito:mockito-core:3.8.0' 26 | } 27 | 28 | group = 'io.github.deblockt' 29 | description = 'json-diff' 30 | gitVersioning.apply { 31 | refs { 32 | branch('.+') { 33 | version = '${ref}-SNAPSHOT' 34 | } 35 | tag('(?.*)') { 36 | version = '${ref.version}' 37 | } 38 | } 39 | 40 | rev { 41 | version = '${commit}' 42 | } 43 | } 44 | 45 | repositories { 46 | mavenCentral() 47 | } 48 | 49 | java { 50 | withJavadocJar() 51 | withSourcesJar() 52 | 53 | sourceCompatibility = JavaLanguageVersion.of(11) 54 | targetCompatibility = JavaLanguageVersion.of(11) 55 | } 56 | 57 | test { 58 | useJUnitPlatform() 59 | } 60 | 61 | nexusPublishing { 62 | repositories { 63 | sonatype { 64 | nexusUrl.set(uri('https://s01.oss.sonatype.org/service/local/')) 65 | snapshotRepositoryUrl.set(uri('https://s01.oss.sonatype.org/content/repositories/snapshots/')) 66 | } 67 | } 68 | } 69 | 70 | publishing { 71 | publications { 72 | mavenJava(MavenPublication) { 73 | artifactId = 'json-diff' 74 | groupId = 'io.github.deblockt' 75 | 76 | from components.java 77 | pom { 78 | name = 'json-diff' 79 | description = 'A customizable lib to perform a json-diff' 80 | url = 'https://github.com/deblockt/json-diff' 81 | 82 | licenses { 83 | license { 84 | name = 'The Apache License, Version 2.0' 85 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 86 | } 87 | } 88 | 89 | developers { 90 | developer { 91 | id = 'deblockt' 92 | name = 'Thomas Deblock' 93 | email = 'deblock.thomas.62@gmail.com' 94 | url = 'https://github.com/deblockt' 95 | } 96 | } 97 | 98 | scm { 99 | connection = 'https://github.com/deblockt/json-diff.git' 100 | developerConnection = 'https://github.com/deblockt/json-diff.git' 101 | url = 'https://github.com/deblockt/json-diff' 102 | } 103 | } 104 | } 105 | } 106 | 107 | signing { 108 | def signingKey = findProperty("signingKey") 109 | def signingPassword = findProperty("signingPassword") 110 | if (signingKey !== null && signingPassword != null) { 111 | useInMemoryPgpKeys(signingKey, signingPassword) 112 | } 113 | 114 | sign publishing.publications.mavenJava 115 | } 116 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deblockt/json-diff/56a38af36423027e254c56a59903064bf5440535/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * This project uses @Incubating APIs which are subject to change. 5 | */ 6 | 7 | rootProject.name = 'json-diff' 8 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/DiffGenerator.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.matcher.JsonMatcher; 5 | import com.deblock.jsondiff.matcher.Path; 6 | import com.fasterxml.jackson.core.JsonProcessingException; 7 | import com.fasterxml.jackson.databind.JsonNode; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | public class DiffGenerator { 14 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 15 | 16 | public static JsonDiff diff(String expected, String actual, JsonMatcher jsonMatcher) { 17 | return jsonMatcher.diff(Path.ROOT, read(expected), read(actual)); 18 | } 19 | 20 | public static List diff(String expected, List actualValues, JsonMatcher jsonMatcher) { 21 | final var expectedObject = read(expected); 22 | return actualValues.stream() 23 | .map(actual -> jsonMatcher.diff(Path.ROOT, expectedObject, read(actual))) 24 | .collect(Collectors.toList()); 25 | } 26 | 27 | private static JsonNode read(String json) { 28 | try { 29 | return OBJECT_MAPPER.readTree(json); 30 | } catch (JsonProcessingException e) { 31 | throw new JsonReadException(e); 32 | } 33 | } 34 | 35 | public static class JsonReadException extends RuntimeException { 36 | 37 | public JsonReadException(Throwable e) { 38 | super(e); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/diff/JsonArrayDiff.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.diff; 2 | 3 | import com.deblock.jsondiff.matcher.Path; 4 | import com.deblock.jsondiff.viewer.JsonDiffViewer; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | public class JsonArrayDiff implements JsonDiff { 11 | private final Map valuesWithoutMatch = new HashMap<>(); 12 | private final Map valuesWithMatch = new HashMap<>(); 13 | private final Map extraValues = new HashMap<>(); 14 | private final Path path; 15 | 16 | public JsonArrayDiff(Path path) { 17 | this.path = path; 18 | } 19 | 20 | public void addNoMatch(int index, JsonNode expectedValue) { 21 | this.valuesWithoutMatch.put(index, expectedValue); 22 | } 23 | 24 | public void addDiff(int index, JsonDiff jsonDiff) { 25 | this.valuesWithMatch.put(index, jsonDiff); 26 | } 27 | 28 | public void addExtraItem(int index, JsonNode extraReceivedValue) { 29 | this.extraValues.put(index, extraReceivedValue); 30 | } 31 | 32 | @Override 33 | public double similarityRate() { 34 | final var totalArraySize = valuesWithoutMatch.size() + valuesWithMatch.size() + extraValues.size(); 35 | if (totalArraySize == 0) { 36 | return 100.0; 37 | } 38 | final var totalSimilarityRate = valuesWithMatch.values().stream() 39 | .mapToDouble(JsonDiff::similarityRate) 40 | .sum(); 41 | 42 | return totalSimilarityRate / totalArraySize; 43 | } 44 | 45 | @Override 46 | public void display(JsonDiffViewer viewer) { 47 | for (final var valuesWithMatch : valuesWithMatch.entrySet()) { 48 | if (valuesWithMatch.getValue().similarityRate() == 100) { 49 | viewer.matchingProperty(path().add(Path.PathItem.of(valuesWithMatch.getKey())), valuesWithMatch.getValue()); 50 | } else { 51 | viewer.nonMatchingProperty(path().add(Path.PathItem.of(valuesWithMatch.getKey())), valuesWithMatch.getValue()); 52 | } 53 | } 54 | 55 | for (final var valuesWithoutMatch : valuesWithoutMatch.entrySet()) { 56 | viewer.missingProperty(path().add(Path.PathItem.of(valuesWithoutMatch.getKey())), valuesWithoutMatch.getValue()); 57 | } 58 | 59 | for (final var extraItem: extraValues.entrySet()) { 60 | viewer.extraProperty(path().add(Path.PathItem.of(extraItem.getKey())), extraItem.getValue()); 61 | } 62 | } 63 | 64 | @Override 65 | public Path path() { 66 | return this.path; 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/diff/JsonDiff.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.diff; 2 | 3 | import com.deblock.jsondiff.matcher.Path; 4 | import com.deblock.jsondiff.viewer.JsonDiffViewer; 5 | 6 | public interface JsonDiff { 7 | /** 8 | * @return the similarity rate (min: 0, max: 100) 9 | */ 10 | double similarityRate(); 11 | 12 | void display(JsonDiffViewer viewer); 13 | 14 | Path path(); 15 | } -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/diff/JsonObjectDiff.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.diff; 2 | 3 | import com.deblock.jsondiff.matcher.Path; 4 | import com.deblock.jsondiff.viewer.JsonDiffViewer; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | import com.fasterxml.jackson.databind.node.ObjectNode; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | public class JsonObjectDiff implements JsonDiff { 12 | private static final int STRUCTURE_MAX_RATIO = 60; 13 | private static final int VALUE_MAX_RATIO = 40; 14 | 15 | private final Map propertiesDiff = new HashMap<>(); 16 | private final Map notFoundProperties = new HashMap<>(); 17 | private final Map extraProperties = new HashMap<>(); 18 | 19 | private final Path path; 20 | 21 | public JsonObjectDiff(Path path) { 22 | this.path = path; 23 | } 24 | 25 | public void addNotFoundProperty(String propertyName, JsonNode value) { 26 | notFoundProperties.put(propertyName, value); 27 | } 28 | 29 | public void addExtraProperty(String propertyName, JsonNode value) { 30 | extraProperties.put(propertyName, value); 31 | } 32 | 33 | public void addPropertyDiff(String propertyName, JsonDiff diff) { 34 | propertiesDiff.put(propertyName, diff); 35 | } 36 | 37 | 38 | @Override 39 | public double similarityRate() { 40 | final var notFoundPropertiesCount = notFoundProperties.keySet().size(); 41 | final var unexpectedPropertiesCount = extraProperties.keySet().size(); 42 | 43 | final var totalPropertiesCount = propertiesDiff.keySet().size() + notFoundPropertiesCount + unexpectedPropertiesCount; 44 | 45 | if (totalPropertiesCount == 0) { 46 | return 100; 47 | } 48 | 49 | final var propertiesSimilarityRate = propertiesDiff.values().stream() 50 | .mapToDouble(JsonDiff::similarityRate) 51 | .sum(); 52 | 53 | final var structureRatio = (totalPropertiesCount - notFoundPropertiesCount - unexpectedPropertiesCount) * STRUCTURE_MAX_RATIO / totalPropertiesCount; 54 | final double equalityRatio; 55 | if (propertiesDiff.isEmpty()) { 56 | equalityRatio = 0; 57 | } else { 58 | equalityRatio = propertiesSimilarityRate * VALUE_MAX_RATIO / (totalPropertiesCount * 100); 59 | } 60 | return structureRatio + equalityRatio; 61 | } 62 | 63 | @Override 64 | public void display(JsonDiffViewer viewer) { 65 | for (final var entry : notFoundProperties.entrySet()) { 66 | viewer.missingProperty(this.path.add(Path.PathItem.of(entry.getKey())), entry.getValue()); 67 | } 68 | for (final var entry : propertiesDiff.entrySet()) { 69 | if (entry.getValue().similarityRate() >= 100) { 70 | viewer.matchingProperty(path().add(Path.PathItem.of(entry.getKey())), entry.getValue()); 71 | } else { 72 | viewer.nonMatchingProperty(path().add(Path.PathItem.of(entry.getKey())), entry.getValue()); 73 | } 74 | } 75 | for (final var entry: extraProperties.entrySet()) { 76 | viewer.extraProperty(path().add(Path.PathItem.of(entry.getKey())), entry.getValue()); 77 | } 78 | final var isEmptyObject = notFoundProperties.isEmpty() && propertiesDiff.isEmpty() && extraProperties.isEmpty(); 79 | if (isEmptyObject) { 80 | viewer.primaryMatching(path(), new ObjectNode(null)); 81 | } 82 | } 83 | 84 | @Override 85 | public Path path() { 86 | return this.path; 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/diff/MatchedPrimaryDiff.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.diff; 2 | 3 | import com.deblock.jsondiff.matcher.Path; 4 | import com.deblock.jsondiff.viewer.JsonDiffViewer; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | 7 | public class MatchedPrimaryDiff implements JsonDiff { 8 | private final JsonNode value; 9 | private final Path path; 10 | 11 | public MatchedPrimaryDiff(Path path, JsonNode value) { 12 | this.value = value; 13 | this.path = path; 14 | } 15 | 16 | @Override 17 | public double similarityRate() { 18 | return 100; 19 | } 20 | 21 | @Override 22 | public void display(JsonDiffViewer viewer) { 23 | viewer.primaryMatching(path, value); 24 | } 25 | 26 | @Override 27 | public Path path() { 28 | return path; 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/diff/UnMatchedPrimaryDiff.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.diff; 2 | 3 | import com.deblock.jsondiff.matcher.Path; 4 | import com.deblock.jsondiff.viewer.JsonDiffViewer; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | 7 | public class UnMatchedPrimaryDiff implements JsonDiff { 8 | private final JsonNode expectedValue; 9 | private final JsonNode receivedValue; 10 | private final Path path; 11 | 12 | public UnMatchedPrimaryDiff(Path path, JsonNode expectedValue, JsonNode receivedValue) { 13 | this.expectedValue = expectedValue; 14 | this.receivedValue = receivedValue; 15 | this.path = path; 16 | } 17 | 18 | @Override 19 | public double similarityRate() { 20 | return 0; 21 | } 22 | 23 | @Override 24 | public void display(JsonDiffViewer viewer) { 25 | viewer.primaryNonMatching(path(), expectedValue, receivedValue); 26 | } 27 | 28 | @Override 29 | public Path path() { 30 | return this.path; 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/matcher/CompositeJsonMatcher.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.*; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.node.ArrayNode; 6 | import com.fasterxml.jackson.databind.node.ObjectNode; 7 | import com.fasterxml.jackson.databind.node.ValueNode; 8 | 9 | public class CompositeJsonMatcher implements JsonMatcher { 10 | private final PartialJsonMatcher jsonArrayPartialMatcher; 11 | private final PartialJsonMatcher jsonObjectPartialMatcher; 12 | private final PartialJsonMatcher primitivePartialMatcher; 13 | 14 | public CompositeJsonMatcher( 15 | PartialJsonMatcher jsonArrayPartialMatcher, 16 | PartialJsonMatcher jsonObjectPartialMatcher, 17 | PartialJsonMatcher primitivePartialMatcher 18 | ) { 19 | this.jsonArrayPartialMatcher = jsonArrayPartialMatcher; 20 | this.jsonObjectPartialMatcher = jsonObjectPartialMatcher; 21 | this.primitivePartialMatcher = primitivePartialMatcher; 22 | } 23 | 24 | @Override 25 | public JsonDiff diff(Path path, JsonNode expected, JsonNode received) { 26 | if (expected instanceof ObjectNode && received instanceof ObjectNode) { 27 | return this.jsonObjectPartialMatcher.jsonDiff(path, (ObjectNode) expected, (ObjectNode) received, this); 28 | } else if (expected instanceof ArrayNode && received instanceof ArrayNode) { 29 | return this.jsonArrayPartialMatcher.jsonDiff(path, (ArrayNode) expected, (ArrayNode) received, this); 30 | } else if (expected instanceof ValueNode && received instanceof ValueNode){ 31 | return this.primitivePartialMatcher.jsonDiff(path, (ValueNode) expected, (ValueNode) received, this); 32 | } else { 33 | return new UnMatchedPrimaryDiff(path, expected, received); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/matcher/JsonMatcher.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.*; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | 6 | public interface JsonMatcher { 7 | 8 | JsonDiff diff(Path path, JsonNode expected, JsonNode received); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/matcher/LenientJsonArrayPartialMatcher.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonArrayDiff; 4 | import com.deblock.jsondiff.diff.JsonDiff; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | import com.fasterxml.jackson.databind.node.ArrayNode; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Comparator; 10 | import java.util.HashMap; 11 | import java.util.HashSet; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.stream.Collectors; 16 | 17 | public class LenientJsonArrayPartialMatcher implements PartialJsonMatcher { 18 | @Override 19 | public JsonDiff jsonDiff(Path path, ArrayNode expectedArrayNode, ArrayNode recievedArrayNode, JsonMatcher jsonMatcher) { 20 | final var diff = new JsonArrayDiff(path); 21 | var mismatches = processMatchingArrayNodesAndReportMismatches(expectedArrayNode, recievedArrayNode, diff, path, jsonMatcher); 22 | if (!mismatches.expectedMissing.isEmpty() || !mismatches.actualMissing.isEmpty()) { 23 | final var diffMap = new HashMap>(); 24 | for (var expectedMissing: mismatches.expectedMissing) { 25 | final var map = new HashMap(); 26 | for (var actualMissing: mismatches.actualMissing) { 27 | map.put( 28 | actualMissing.index, 29 | jsonMatcher.diff( 30 | path.add(Path.PathItem.of(expectedMissing.index)), expectedMissing.jsonNode, actualMissing.jsonNode 31 | ) 32 | ); 33 | } 34 | diffMap.put(expectedMissing.index, map); 35 | } 36 | 37 | final var entrySortedByBestMatch = 38 | diffMap.entrySet().stream() 39 | .sorted(Comparator.comparingDouble(this::maxSimilarityRate).reversed()) 40 | .toList(); 41 | final var alreadyMatchedIndex = new HashSet(); 42 | 43 | for (final var entry : entrySortedByBestMatch) { 44 | final var matchedItem = entry.getValue().entrySet().stream() 45 | .filter(e -> !alreadyMatchedIndex.contains(e.getKey())) 46 | .max(Comparator.comparingDouble(e -> e.getValue().similarityRate())); 47 | 48 | if (matchedItem.isEmpty()) { 49 | diff.addNoMatch(entry.getKey(), expectedArrayNode.get(entry.getKey())); 50 | } else { 51 | diff.addDiff(entry.getKey(), matchedItem.get().getValue()); 52 | alreadyMatchedIndex.add(matchedItem.get().getKey()); 53 | } 54 | } 55 | 56 | if (alreadyMatchedIndex.size() < recievedArrayNode.size()) { 57 | final var receivedIndex = mismatches.actualMissing.stream().map(it -> it.index).collect(Collectors.toList()); 58 | receivedIndex.removeAll(alreadyMatchedIndex); 59 | receivedIndex.forEach(index -> diff.addExtraItem(index, recievedArrayNode.get(index))); 60 | } 61 | } 62 | 63 | return diff; 64 | } 65 | 66 | private double maxSimilarityRate(Map.Entry> entry) { 67 | return entry.getValue().values().stream().mapToDouble(JsonDiff::similarityRate).max().orElse(0); 68 | } 69 | 70 | private MismatchPair, List> processMatchingArrayNodesAndReportMismatches(ArrayNode expectedArrayNode, ArrayNode actualArrayNode, JsonArrayDiff diff, Path path, JsonMatcher jsonMatcher) { 71 | if (actualArrayNode.equals(expectedArrayNode)) { 72 | for (int i = 0; i < expectedArrayNode.size(); i++) { 73 | diff.addDiff(i, jsonMatcher.diff(path.add(Path.PathItem.of(i)), expectedArrayNode.get(i), expectedArrayNode.get(i))); 74 | } 75 | return new MismatchPair<>(List.of(), List.of()); 76 | } 77 | List expectedMissing = new ArrayList<>(); 78 | List actualMissing = new ArrayList<>(); 79 | NodeCounter expectedNodeCounter = getElementsWithCount(expectedArrayNode.elements()); 80 | NodeCounter actualNodeCounter = getElementsWithCount(actualArrayNode.elements()); 81 | 82 | for (int i = 0; i < expectedArrayNode.size(); i++) { 83 | var expectedElement = expectedArrayNode.get(i); 84 | if (actualNodeCounter.containsNode(expectedElement)) { 85 | actualNodeCounter.removeNode(expectedElement); 86 | diff.addDiff(i, jsonMatcher.diff(path.add(Path.PathItem.of(i)), expectedElement, expectedElement)); 87 | } else { 88 | expectedMissing.add(new IndexedJsonNode(i, expectedArrayNode.get(i))); 89 | } 90 | } 91 | for (int i = 0; i < actualArrayNode.size(); i++) { 92 | var actualElement = actualArrayNode.get(i); 93 | if (expectedNodeCounter.containsNode(actualElement)) { 94 | expectedNodeCounter.removeNode(actualElement); 95 | } else { 96 | actualMissing.add(new IndexedJsonNode(i, actualArrayNode.get(i))); 97 | } 98 | } 99 | return new MismatchPair<>(expectedMissing, actualMissing); 100 | } 101 | 102 | private NodeCounter getElementsWithCount(Iterator elements) { 103 | var nodeCounter = new NodeCounter(); 104 | elements.forEachRemaining(nodeCounter::addNode); 105 | return nodeCounter; 106 | } 107 | 108 | private static class MismatchPair { 109 | private final K expectedMissing; 110 | private final V actualMissing; 111 | 112 | public MismatchPair(K expectedMissing, V actualMissing) { 113 | this.expectedMissing = expectedMissing; 114 | this.actualMissing = actualMissing; 115 | } 116 | } 117 | 118 | private static class IndexedJsonNode { 119 | private final int index; 120 | private final JsonNode jsonNode; 121 | 122 | public IndexedJsonNode(int index, JsonNode jsonNode) { 123 | this.index = index; 124 | this.jsonNode = jsonNode; 125 | } 126 | } 127 | 128 | private static class NodeCounter { 129 | private Map nodeCounter = new HashMap<>(); 130 | 131 | public void addNode(JsonNode node) { 132 | nodeCounter.compute(node, (key, prevValue) -> (prevValue == null ? 0 : prevValue) + 1); 133 | } 134 | 135 | public void removeNode(JsonNode node) { 136 | nodeCounter.put(node, nodeCounter.get(node) - 1); 137 | if (nodeCounter.get(node) == 0) { 138 | nodeCounter.remove(node); 139 | } 140 | } 141 | 142 | public boolean containsNode(JsonNode node) { 143 | return nodeCounter.containsKey(node); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/matcher/LenientJsonObjectPartialMatcher.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.diff.JsonObjectDiff; 5 | import com.fasterxml.jackson.databind.node.ObjectNode; 6 | 7 | public class LenientJsonObjectPartialMatcher implements PartialJsonMatcher { 8 | 9 | @Override 10 | public JsonDiff jsonDiff(Path path, ObjectNode expectedJson, ObjectNode receivedJson, JsonMatcher jsonMatcher) { 11 | final var jsonDiff = new JsonObjectDiff(path); 12 | 13 | expectedJson.fields() 14 | .forEachRemaining(entry -> { 15 | final var expectedPropertyName = entry.getKey(); 16 | final var expectedValue = entry.getValue(); 17 | final var receivedValue = receivedJson.get(expectedPropertyName); 18 | 19 | if (receivedValue == null) { 20 | jsonDiff.addNotFoundProperty(expectedPropertyName, expectedValue); 21 | } else { 22 | final var diff = jsonMatcher.diff(path.add(Path.PathItem.of(expectedPropertyName)), expectedValue, receivedValue); 23 | jsonDiff.addPropertyDiff(expectedPropertyName, diff); 24 | } 25 | }); 26 | 27 | return jsonDiff; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcher.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.diff.MatchedPrimaryDiff; 5 | import com.deblock.jsondiff.diff.UnMatchedPrimaryDiff; 6 | import com.fasterxml.jackson.databind.node.NumericNode; 7 | import com.fasterxml.jackson.databind.node.ValueNode; 8 | 9 | public class LenientNumberPrimitivePartialMatcher implements PartialJsonMatcher { 10 | private final PartialJsonMatcher delegated; 11 | 12 | public LenientNumberPrimitivePartialMatcher(PartialJsonMatcher delegated) { 13 | this.delegated = delegated; 14 | } 15 | 16 | @Override 17 | public JsonDiff jsonDiff(Path path, ValueNode expectedValue, ValueNode receivedValue, JsonMatcher jsonMatcher) { 18 | if (expectedValue instanceof NumericNode && receivedValue instanceof NumericNode) { 19 | final var expectedIntValue = expectedValue.intValue(); 20 | final var actualIntValue = receivedValue.intValue(); 21 | final var expectedDecimalValue = receivedValue.doubleValue() % 1; 22 | final var actualDecimalValue = expectedValue.doubleValue() % 1; 23 | 24 | if (expectedIntValue != actualIntValue || expectedDecimalValue != actualDecimalValue) { 25 | return new UnMatchedPrimaryDiff(path, expectedValue, receivedValue); 26 | } else { 27 | return new MatchedPrimaryDiff(path, expectedValue); 28 | } 29 | } 30 | 31 | return delegated.jsonDiff(path, expectedValue, receivedValue, jsonMatcher); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/matcher/PartialJsonMatcher.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | 6 | public interface PartialJsonMatcher { 7 | JsonDiff jsonDiff(Path path, T expectedJson, T receivedJson, JsonMatcher jsonMatcher); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/matcher/Path.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import java.util.Objects; 4 | 5 | public class Path { 6 | public static final Path ROOT = new Path(); 7 | 8 | public final PathItem property; 9 | public final Path next; 10 | 11 | public Path() { 12 | this(null, null); 13 | } 14 | 15 | private Path(PathItem property, Path next) { 16 | this.property = property; 17 | this.next = next; 18 | } 19 | 20 | private Path(PathItem property) { 21 | this.property = property; 22 | this.next = null; 23 | } 24 | 25 | public Path add(PathItem item) { 26 | if (this.next == null) { 27 | return new Path(this.property, new Path(item)); 28 | } else { 29 | return new Path(this.property, this.next.add(item)); 30 | } 31 | } 32 | 33 | public String toString() { 34 | return ((this.property == null) ? "$" : this.property) + 35 | ((this.next == null) ? "" : "." + this.next); 36 | } 37 | 38 | @Override 39 | public boolean equals(Object o) { 40 | if (this == o) return true; 41 | if (o == null || getClass() != o.getClass()) return false; 42 | Path path = (Path) o; 43 | return Objects.equals(property, path.property) && Objects.equals(next, path.next); 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return Objects.hash(property, next); 49 | } 50 | 51 | public interface PathItem { 52 | static PathItem of(String property) { 53 | return new ObjectProperty(property); 54 | } 55 | 56 | static PathItem of(Integer index) { 57 | return new ArrayIndex(index); 58 | } 59 | 60 | class ArrayIndex implements PathItem { 61 | public final int index; 62 | 63 | public ArrayIndex(int index) { 64 | this.index = index; 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return String.valueOf(index); 70 | } 71 | 72 | @Override 73 | public boolean equals(Object o) { 74 | if (this == o) return true; 75 | if (o == null || getClass() != o.getClass()) return false; 76 | ArrayIndex that = (ArrayIndex) o; 77 | return index == that.index; 78 | } 79 | 80 | @Override 81 | public int hashCode() { 82 | return Objects.hash(index); 83 | } 84 | } 85 | 86 | class ObjectProperty implements PathItem { 87 | public final String property; 88 | 89 | public ObjectProperty(String property) { 90 | this.property = property; 91 | } 92 | 93 | @Override 94 | public String toString() { 95 | return this.property; 96 | } 97 | 98 | @Override 99 | public boolean equals(Object o) { 100 | if (this == o) return true; 101 | if (o == null || getClass() != o.getClass()) return false; 102 | ObjectProperty that = (ObjectProperty) o; 103 | return Objects.equals(property, that.property); 104 | } 105 | 106 | @Override 107 | public int hashCode() { 108 | return Objects.hash(property); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/matcher/StrictJsonArrayPartialMatcher.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonArrayDiff; 4 | import com.deblock.jsondiff.diff.JsonDiff; 5 | import com.fasterxml.jackson.databind.node.ArrayNode; 6 | 7 | import java.util.Comparator; 8 | import java.util.HashMap; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.IntStream; 13 | 14 | public class StrictJsonArrayPartialMatcher implements PartialJsonMatcher { 15 | @Override 16 | public JsonDiff jsonDiff(Path path, ArrayNode expectedValues, ArrayNode receivedValues, JsonMatcher jsonMatcher) { 17 | final var diff = new JsonArrayDiff(path); 18 | 19 | for (int i = 0; i < expectedValues.size() && i < receivedValues.size(); i++) { 20 | final var expectedValue = expectedValues.get(i); 21 | final var receivedValue = receivedValues.get(i); 22 | 23 | final var valueDiff = jsonMatcher.diff(path.add(Path.PathItem.of(i)), expectedValue, receivedValue); 24 | diff.addDiff(i, valueDiff); 25 | } 26 | 27 | if (expectedValues.size() > receivedValues.size()) { 28 | for (int i = receivedValues.size(); i < expectedValues.size(); i++) { 29 | diff.addNoMatch(i, expectedValues.get(i)); 30 | } 31 | } else if (expectedValues.size() < receivedValues.size()) { 32 | for (int i = expectedValues.size(); i < receivedValues.size(); i++) { 33 | diff.addExtraItem(i, receivedValues.get(i)); 34 | } 35 | } 36 | return diff; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/matcher/StrictJsonObjectPartialMatcher.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.diff.JsonObjectDiff; 5 | import com.fasterxml.jackson.databind.node.ObjectNode; 6 | 7 | import java.util.stream.Collectors; 8 | import java.util.stream.StreamSupport; 9 | 10 | public class StrictJsonObjectPartialMatcher implements PartialJsonMatcher { 11 | 12 | @Override 13 | public JsonDiff jsonDiff(Path path, ObjectNode expectedJson, ObjectNode receivedJson, JsonMatcher jsonMatcher) { 14 | final var jsonDiff = new JsonObjectDiff(path); 15 | final var receivedJsonFields = StreamSupport.stream(((Iterable) receivedJson::fieldNames).spliterator(), false).collect(Collectors.toSet()); 16 | 17 | expectedJson.fields() 18 | .forEachRemaining(entry -> { 19 | final var expectedPropertyName = entry.getKey(); 20 | final var expectedValue = entry.getValue(); 21 | final var receivedValue = receivedJson.get(expectedPropertyName); 22 | 23 | if (receivedValue == null) { 24 | jsonDiff.addNotFoundProperty(expectedPropertyName, expectedValue); 25 | } else { 26 | final var diff = jsonMatcher.diff(path.add(Path.PathItem.of(expectedPropertyName)), expectedValue, receivedValue); 27 | jsonDiff.addPropertyDiff(expectedPropertyName, diff); 28 | } 29 | receivedJsonFields.remove(expectedPropertyName); 30 | }); 31 | 32 | 33 | receivedJson.fields() 34 | .forEachRemaining(entry -> { 35 | final var receivedPropertyName = entry.getKey(); 36 | final var receivedPropertyValue = entry.getValue(); 37 | final var expectedValue = expectedJson.get(receivedPropertyName); 38 | 39 | if (expectedValue == null) { 40 | jsonDiff.addExtraProperty(receivedPropertyName, receivedPropertyValue); 41 | } 42 | }); 43 | 44 | return jsonDiff; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/matcher/StrictPrimitivePartialMatcher.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.diff.MatchedPrimaryDiff; 5 | import com.deblock.jsondiff.diff.UnMatchedPrimaryDiff; 6 | import com.fasterxml.jackson.databind.node.ValueNode; 7 | 8 | import java.util.Objects; 9 | 10 | public class StrictPrimitivePartialMatcher implements PartialJsonMatcher { 11 | 12 | @Override 13 | public JsonDiff jsonDiff(Path path, ValueNode expectedValue, ValueNode receivedValue, JsonMatcher jsonMatcher) { 14 | if (Objects.equals(expectedValue, receivedValue)) { 15 | return new MatchedPrimaryDiff(path, expectedValue); 16 | } else { 17 | return new UnMatchedPrimaryDiff(path, expectedValue, receivedValue); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/viewer/JsonDiffViewer.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.viewer; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.matcher.Path; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | 7 | public interface JsonDiffViewer { 8 | 9 | void matchingProperty(Path path, JsonDiff diff); 10 | 11 | void nonMatchingProperty(Path path, JsonDiff diff); 12 | 13 | void missingProperty(Path path, JsonNode value); 14 | 15 | void extraProperty(Path path, JsonNode extraReceivedValue); 16 | 17 | void primaryNonMatching(Path path, JsonNode expected, JsonNode value); 18 | 19 | void primaryMatching(Path path, JsonNode value); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/viewer/OnlyErrorDiffViewer.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.viewer; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.matcher.Path; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | 7 | /** 8 | * List all error on a string 9 | * call .toString to get the error string 10 | */ 11 | public class OnlyErrorDiffViewer implements JsonDiffViewer { 12 | private final StringBuilder stringBuilder = new StringBuilder(); 13 | 14 | @Override 15 | public void matchingProperty(Path path, JsonDiff value) { } 16 | 17 | @Override 18 | public void primaryMatching(Path path, JsonNode value) { 19 | 20 | } 21 | 22 | @Override 23 | public void nonMatchingProperty(Path path, JsonDiff diff) { 24 | diff.display(this); 25 | } 26 | 27 | @Override 28 | public void missingProperty(Path path, JsonNode value) { 29 | stringBuilder 30 | .append("The property \"") 31 | .append(path) 32 | .append("\" in the expected json is not found\n"); 33 | } 34 | 35 | @Override 36 | public void extraProperty(Path path, JsonNode extraReceivedValue) { 37 | stringBuilder 38 | .append("The property \"") 39 | .append(path) 40 | .append("\" in the received json is not expected\n"); 41 | } 42 | 43 | @Override 44 | public void primaryNonMatching(Path path, JsonNode expected, JsonNode value) { 45 | stringBuilder 46 | .append("The property \"").append(path) 47 | .append("\" didn't match. Expected ").append(expected) 48 | .append(", Received: ").append(value) 49 | .append("\n"); 50 | } 51 | 52 | public String toString() { 53 | return stringBuilder.toString(); 54 | } 55 | 56 | public static JsonDiffViewer from(JsonDiff jsonDiff) { 57 | final var result = new OnlyErrorDiffViewer(); 58 | jsonDiff.display(result); 59 | return result; 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/java/com/deblock/jsondiff/viewer/PatchDiffViewer.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.viewer; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.matcher.Path; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | /** 15 | * This is an experimental feature 16 | * 17 | * create a patch file from a json diff 18 | * call .toString to get the patch string 19 | */ 20 | public class PatchDiffViewer implements JsonDiffViewer { 21 | private Object diff = null; 22 | 23 | @Override 24 | public void matchingProperty(Path path, JsonDiff value) { 25 | value.display(this); 26 | } 27 | 28 | @Override 29 | public void primaryMatching(Path path, JsonNode value) { 30 | this.diff = this.addPath(diff, path, new DiffValue.MatchingProperty(value)); 31 | } 32 | 33 | @Override 34 | public void nonMatchingProperty(Path path, JsonDiff diff) { 35 | diff.display(this); 36 | } 37 | 38 | @Override 39 | public void missingProperty(Path path, JsonNode value) { 40 | this.diff = this.addPath(diff, path, new DiffValue.MissingProperty(value)); 41 | } 42 | 43 | @Override 44 | public void extraProperty(Path path, JsonNode extraReceivedValue) { 45 | this.diff = this.addPath(diff, path, new DiffValue.ExtraProperty(extraReceivedValue)); 46 | } 47 | 48 | @Override 49 | public void primaryNonMatching(Path path, JsonNode expected, JsonNode value) { 50 | this.diff = this.addPath(diff, path, new DiffValue.NonMatchingProperty(expected, value)); 51 | } 52 | 53 | public Object addPath(Object root, Path path, DiffValue diffValue) { 54 | if (path == null) { 55 | return diffValue; 56 | } else if (path.property instanceof Path.PathItem.ArrayIndex) { 57 | final var index = ((Path.PathItem.ArrayIndex) path.property).index; 58 | if (root == null) { 59 | final var newRoot = new DiffValue.ArrayDiff(); 60 | newRoot.set(index, this.addPath(null, path.next, diffValue)); 61 | return newRoot; 62 | } else if (root instanceof DiffValue.ArrayDiff) { 63 | final var array = (DiffValue.ArrayDiff) root; 64 | if (array.hasIndex(index) && !(diffValue instanceof DiffValue.ExtraProperty)) { 65 | this.addPath(array.get(index), path.next, diffValue); 66 | } else { 67 | array.set(index, this.addPath(array.get(index), path.next, diffValue)); 68 | } 69 | return array; 70 | } else { 71 | throw new IllegalArgumentException("The path " + path + " is not an array"); 72 | } 73 | } else if (path.property instanceof Path.PathItem.ObjectProperty) { 74 | final var propertyName = ((Path.PathItem.ObjectProperty) path.property).property; 75 | if (root == null) { 76 | final var newRoot = new HashMap(); 77 | newRoot.put(propertyName, this.addPath(null, path.next, diffValue)); 78 | return newRoot; 79 | } else if (root instanceof Map) { 80 | final var map = (Map) root; 81 | map.put(propertyName, this.addPath(map.get(propertyName), path.next, diffValue)); 82 | return map; 83 | } else { 84 | throw new IllegalArgumentException("The path " + path + " is not an object"); 85 | } 86 | } else if (path.property == null) { 87 | if (path.next != null) { 88 | return this.addPath(root, path.next, diffValue); 89 | } else { 90 | return diff; 91 | } 92 | } else { 93 | throw new IllegalArgumentException("Unsupported path type " + path.property.getClass()); 94 | } 95 | } 96 | 97 | public String toString() { 98 | return "--- actual\n+++ expected\n@@ @@\n" + toDiff(this.diff, "", "", "", ""); 99 | } 100 | 101 | private String toDiff(Object diff, String indent, String startOfLine, String endOfLineExpected, String endOfLineActual) { 102 | if (diff instanceof DiffValue.ArrayDiff) { 103 | final var arrayContent = new StringBuilder(); 104 | final var allObjects = ((DiffValue.ArrayDiff) diff).allObjects(); 105 | for (int i = 0; i < allObjects.size(); ++i) { 106 | final var object = allObjects.get(i); 107 | arrayContent 108 | .append(toDiff( 109 | object, 110 | indent + " ", 111 | indent + " ", 112 | commaIfHasNextExpectedProperty(i + 1, allObjects), 113 | commaIfHasNextActualProperty(i + 1, allObjects) 114 | )) 115 | .append("\n"); 116 | } 117 | return startOfLine + " [\n" + arrayContent + indent + " ]" + endOfLineExpected; 118 | } else if (diff instanceof Map) { 119 | final var objectContent = new StringBuilder(); 120 | final var diffObject = (Map) diff; 121 | final var keys = new ArrayList<>(diffObject.keySet()); 122 | for (int i = 0; i < keys.size(); ++i) { 123 | final var object = diffObject.get(keys.get(i)); 124 | final var isObjectNotADiff = object instanceof DiffValue.ArrayDiff || object instanceof Map; 125 | final var propertyPrefix = indent + " \"" + keys.get(i) + "\":" + ((isObjectNotADiff) ? "" : " "); 126 | objectContent 127 | .append(isObjectNotADiff ? " " + propertyPrefix : "") 128 | .append(toDiff( 129 | object, 130 | isObjectNotADiff ? indent + " " : propertyPrefix, 131 | "", 132 | commaIfHasNextExpectedProperty(i + 1, diffObject, keys), 133 | commaIfHasNextActualProperty(i + 1, diffObject, keys) 134 | )) 135 | .append("\n"); 136 | } 137 | return startOfLine + " {\n" + objectContent + indent + " }" + endOfLineExpected; 138 | } else if (diff instanceof DiffValue.MatchingProperty) { 139 | if (endOfLineActual.equals(endOfLineExpected)) { 140 | return " " + indent + ((DiffValue.MatchingProperty) diff).value.toString() + endOfLineActual; 141 | } else { 142 | return "-" + indent + ((DiffValue.MatchingProperty) diff).value.toString() + endOfLineActual + "\n" + 143 | "+" + indent + ((DiffValue.MatchingProperty) diff).value.toString() + endOfLineExpected; 144 | } 145 | } else if (diff instanceof DiffValue.MissingProperty) { 146 | return "+" + indent + ((DiffValue.MissingProperty) diff).value.toString() + endOfLineExpected; 147 | } else if (diff instanceof DiffValue.ExtraProperty) { 148 | return "-" + indent + ((DiffValue.ExtraProperty) diff).value.toString() + endOfLineActual; 149 | } else if (diff instanceof DiffValue.NonMatchingProperty) { 150 | final var value = ((DiffValue.NonMatchingProperty) diff); 151 | return "-" + indent + value.value.toString() + endOfLineActual + "\n+" + indent + value.expected.toString() + endOfLineExpected; 152 | } else { 153 | throw new IllegalArgumentException("Unsupported diff type " + diff.getClass()); 154 | } 155 | } 156 | 157 | 158 | private String commaIfHasNextExpectedProperty(int index, Map diffObject, ArrayList keys) { 159 | for (int i = index; i < keys.size(); ++i) { 160 | if (isExpectedPart(diffObject.get(keys.get(i)))) { 161 | return ","; 162 | } 163 | } 164 | return ""; 165 | } 166 | 167 | private String commaIfHasNextActualProperty(int index, Map diffObject, ArrayList keys) { 168 | for (int i = index; i < keys.size(); ++i) { 169 | if (isActualPart(diffObject.get(keys.get(i)))) { 170 | return ","; 171 | } 172 | } 173 | return ""; 174 | } 175 | 176 | private String commaIfHasNextExpectedProperty(int index, List allObjects) { 177 | for (int i = index; i < allObjects.size(); ++i) { 178 | if (isExpectedPart(allObjects.get(i))) { 179 | return ","; 180 | } 181 | } 182 | return ""; 183 | } 184 | 185 | private String commaIfHasNextActualProperty(int index, List allObjects) { 186 | for (int i = index; i < allObjects.size(); ++i) { 187 | if (isActualPart(allObjects.get(i))) { 188 | return ","; 189 | } 190 | } 191 | return ""; 192 | } 193 | 194 | public boolean isActualPart(Object object) { 195 | return object instanceof DiffValue.NonMatchingProperty 196 | || object instanceof DiffValue.ExtraProperty 197 | || object instanceof DiffValue.MatchingProperty 198 | || object instanceof Map 199 | || object instanceof DiffValue.ArrayDiff; 200 | } 201 | 202 | public boolean isExpectedPart(Object object) { 203 | return object instanceof DiffValue.NonMatchingProperty 204 | || object instanceof DiffValue.MissingProperty 205 | || object instanceof DiffValue.MatchingProperty 206 | || object instanceof Map 207 | || object instanceof DiffValue.ArrayDiff; 208 | } 209 | 210 | public static class DiffValue { 211 | 212 | public static class ArrayDiff extends DiffValue { 213 | public final List diffs = new ArrayList<>(); 214 | public final List extraProperty = new ArrayList<>(); 215 | 216 | public void set(int index, Object object) { 217 | if (object instanceof DiffValue.ExtraProperty) { 218 | extraProperty.add((DiffValue.ExtraProperty) object); 219 | } else { 220 | while (diffs.size() <= index) { 221 | diffs.add(null); 222 | } 223 | diffs.set(index, object); 224 | } 225 | } 226 | 227 | public boolean hasIndex(int index) { 228 | return this.diffs.size() > index && this.diffs.get(index) != null; 229 | } 230 | 231 | public Object get(int index) { 232 | if (!this.hasIndex(index)) { 233 | return null; 234 | } 235 | return this.diffs.get(index); 236 | } 237 | 238 | public List allObjects() { 239 | return Stream.concat(this.diffs.stream(), this.extraProperty.stream()) 240 | .collect(Collectors.toList()); 241 | } 242 | } 243 | 244 | public static class MatchingProperty extends DiffValue { 245 | public final JsonNode value; 246 | 247 | public MatchingProperty(JsonNode value) { 248 | this.value = value; 249 | } 250 | } 251 | 252 | public static class MissingProperty extends DiffValue { 253 | public final JsonNode value; 254 | 255 | public MissingProperty(JsonNode value) { 256 | this.value = value; 257 | } 258 | } 259 | 260 | public static class ExtraProperty extends DiffValue { 261 | public final JsonNode value; 262 | 263 | public ExtraProperty(JsonNode extraReceivedValue) { 264 | this.value = extraReceivedValue; 265 | } 266 | } 267 | 268 | public static class NonMatchingProperty extends DiffValue { 269 | public final JsonNode value; 270 | public final JsonNode expected; 271 | 272 | public NonMatchingProperty(JsonNode expected, JsonNode value) { 273 | this.expected = expected; 274 | this.value = value; 275 | } 276 | } 277 | } 278 | 279 | public static PatchDiffViewer from(JsonDiff jsonDiff) { 280 | final var result = new PatchDiffViewer(); 281 | jsonDiff.display(result); 282 | return result; 283 | } 284 | } -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/DiffGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.matcher.JsonMatcher; 5 | import com.deblock.jsondiff.matcher.Path; 6 | import com.fasterxml.jackson.databind.node.ArrayNode; 7 | import com.fasterxml.jackson.databind.node.ObjectNode; 8 | import com.fasterxml.jackson.databind.node.TextNode; 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.Mockito; 12 | 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | 18 | public class DiffGeneratorTest { 19 | 20 | @Test 21 | public void shouldThrowJsonReadExceptionWithJsonIsMalformed() { 22 | final var expectedJson = "{}"; 23 | final var receivedJson = "{"; 24 | 25 | Assertions.assertThrows(DiffGenerator.JsonReadException.class, () -> DiffGenerator.diff(expectedJson, receivedJson, null)); 26 | Assertions.assertThrows(DiffGenerator.JsonReadException.class, () -> DiffGenerator.diff(receivedJson, expectedJson, null)); 27 | Assertions.assertThrows(DiffGenerator.JsonReadException.class, () -> DiffGenerator.diff(receivedJson, List.of(expectedJson), null)); 28 | Assertions.assertThrows(DiffGenerator.JsonReadException.class, () -> DiffGenerator.diff(expectedJson, List.of(receivedJson), null)); 29 | } 30 | 31 | @Test 32 | public void shouldSupportStringDiff() { 33 | final var expectedJson = "\"string1\""; 34 | final var receivedJson = "\"string1\""; 35 | final var matcher = Mockito.mock(JsonMatcher.class); 36 | final var diff = Mockito.mock(JsonDiff.class); 37 | Mockito.when(matcher.diff(Path.ROOT, TextNode.valueOf("string1"), TextNode.valueOf("string1"))).thenReturn(diff); 38 | 39 | final var result = DiffGenerator.diff(expectedJson, receivedJson, matcher); 40 | 41 | assertEquals(diff, result); 42 | } 43 | 44 | @Test 45 | public void shouldSupportArrayDiff() { 46 | final var expectedJson = "[\"string1\"]"; 47 | final var receivedJson = "[\"string1\"]"; 48 | final var jsonNode = new ArrayNode(null, List.of(TextNode.valueOf("string1"))); 49 | final var matcher = Mockito.mock(JsonMatcher.class); 50 | final var diff = Mockito.mock(JsonDiff.class); 51 | Mockito.when(matcher.diff(Path.ROOT, jsonNode, jsonNode)).thenReturn(diff); 52 | 53 | final var result = DiffGenerator.diff(expectedJson, receivedJson, matcher); 54 | 55 | assertEquals(diff, result); 56 | } 57 | 58 | @Test 59 | public void shouldSupportObjectDiff() { 60 | final var expectedJson = "{\"a\": \"string1\"}"; 61 | final var receivedJson = "{\"a\": \"string1\"}"; 62 | final var jsonNode = new ObjectNode(null, Map.of("a", TextNode.valueOf("string1"))); 63 | final var matcher = Mockito.mock(JsonMatcher.class); 64 | final var diff = Mockito.mock(JsonDiff.class); 65 | Mockito.when(matcher.diff(Path.ROOT, jsonNode, jsonNode)).thenReturn(diff); 66 | 67 | final var result = DiffGenerator.diff(expectedJson, receivedJson, matcher); 68 | 69 | assertEquals(diff, result); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/Sample.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff; 2 | 3 | import com.deblock.jsondiff.matcher.*; 4 | import com.deblock.jsondiff.viewer.OnlyErrorDiffViewer; 5 | import com.deblock.jsondiff.viewer.PatchDiffViewer; 6 | 7 | public class Sample { 8 | public static void main(String[] args) { 9 | final var expectedJson = "{\"array\": [{\"b\": [1]}, {\"a\": [1, 5]}]}"; 10 | final var receivedJson = "{\"array\": [{\"a\": [1]}]}"; 11 | 12 | // define your matcher 13 | // CompositeJsonMatcher use other matcher to perform matching on objects, list or primitive 14 | final var jsonMatcher = new CompositeJsonMatcher( 15 | new LenientJsonArrayPartialMatcher(), // comparing array using lenient mode (ignore array order and extra items) 16 | new LenientJsonObjectPartialMatcher(), // comparing object using lenient mode (ignoring extra properties) 17 | new LenientNumberPrimitivePartialMatcher(new StrictPrimitivePartialMatcher()) // comparing primitive types and manage numbers (100.00 == 100) 18 | ); 19 | 20 | // generate a diff 21 | final var jsondiff = DiffGenerator.diff(expectedJson, receivedJson, jsonMatcher); 22 | 23 | // use the viewer to collect diff data 24 | final var errorsResult = PatchDiffViewer.from(jsondiff); 25 | 26 | // print the diff result 27 | System.out.println(errorsResult); 28 | // print a similarity ratio between expected and received json (0 <= ratio <= 100) 29 | System.out.println(jsondiff.similarityRate()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/matcher/CompositeJsonMatcherTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.fasterxml.jackson.databind.node.*; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mockito; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | public class CompositeJsonMatcherTest { 11 | private final static Path path = Path.ROOT.add(Path.PathItem.of("property")); 12 | 13 | @Test 14 | public void shouldCallTheArrayMatcherIfTheTwoObjectAreArray() { 15 | final var array1 = new ArrayNode(null); 16 | final var array2 = new ArrayNode(null); 17 | 18 | final var arrayMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); 19 | final var compositeMatcher = new CompositeJsonMatcher( 20 | arrayMatcher, 21 | (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class), 22 | (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class) 23 | ); 24 | final var expectedJsonDiff = Mockito.mock(JsonDiff.class); 25 | Mockito.when(arrayMatcher.jsonDiff(path, array1, array2, compositeMatcher)).thenReturn(expectedJsonDiff); 26 | 27 | final var result = compositeMatcher.diff(path, array1, array2); 28 | 29 | assertEquals(expectedJsonDiff, result); 30 | } 31 | 32 | @Test 33 | public void shouldCallTheObjectMatcherIfTheTwoObjectAreObject() { 34 | final var object1 = new ObjectNode(null); 35 | final var object2 = new ObjectNode(null); 36 | 37 | final var objectMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); 38 | final var compositeMatcher = new CompositeJsonMatcher( 39 | (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class), 40 | objectMatcher, 41 | (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class) 42 | ); 43 | final var expectedJsonDiff = Mockito.mock(JsonDiff.class); 44 | Mockito.when(objectMatcher.jsonDiff(path, object1, object2, compositeMatcher)).thenReturn(expectedJsonDiff); 45 | 46 | final var result = compositeMatcher.diff(path, object1, object2); 47 | 48 | assertEquals(expectedJsonDiff, result); 49 | } 50 | 51 | @Test 52 | public void shouldCallThePrimitiveMatcherIfTheTwoObjectAreValue() { 53 | final var value1 = TextNode.valueOf(""); 54 | final var value2 = IntNode.valueOf(10); 55 | 56 | final var primitiveMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); 57 | final var compositeMatcher = new CompositeJsonMatcher( 58 | (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class), 59 | (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class), 60 | primitiveMatcher 61 | ); 62 | final var expectedJsonDiff = Mockito.mock(JsonDiff.class); 63 | Mockito.when(primitiveMatcher.jsonDiff(path, value1, value2, compositeMatcher)).thenReturn(expectedJsonDiff); 64 | 65 | final var result = compositeMatcher.diff(path, value1, value2); 66 | 67 | assertEquals(expectedJsonDiff, result); 68 | } 69 | 70 | @Test 71 | public void shouldReturnANonMatchWhenTypesAreDifferent() { 72 | final var value1 = TextNode.valueOf(""); 73 | final var value2 = new ObjectNode(null); 74 | 75 | final var compositeMatcher = new CompositeJsonMatcher( 76 | (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class), 77 | (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class), 78 | (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class) 79 | ); 80 | 81 | final var result = compositeMatcher.diff(path, value1, value2); 82 | 83 | assertEquals(0, result.similarityRate()); 84 | assertEquals(path, result.path()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/matcher/JsonDiffAsserter.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.viewer.JsonDiffViewer; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | 13 | public class JsonDiffAsserter implements JsonDiffViewer { 14 | private final List missingPropertyAsserters = new ArrayList<>(); 15 | private final List extraPropertyAsserters = new ArrayList<>(); 16 | private final List nonMatchingPropertyAsserters = new ArrayList<>(); 17 | private final List matchingPropertyAsserters = new ArrayList<>(); 18 | private final List primaryNonMatchingAsserters = new ArrayList<>(); 19 | private final List primaryMatchingAsserters = new ArrayList<>(); 20 | private Double expectedSimilarityRate = null; 21 | 22 | @Override 23 | public void matchingProperty(Path path, JsonDiff diff) { 24 | for (final var asserter : matchingPropertyAsserters) { 25 | if (asserter.done(diff.path())) { 26 | return; 27 | } 28 | } 29 | throw new AssertionError("Non expected matching property " + diff.path()); 30 | } 31 | 32 | @Override 33 | public void nonMatchingProperty(Path path, JsonDiff diff) { 34 | for (final var asserter : nonMatchingPropertyAsserters) { 35 | if (asserter.done(diff.path())) { 36 | return; 37 | } 38 | } 39 | throw new AssertionError("Non expected non matching property " + diff.path()); 40 | } 41 | 42 | @Override 43 | public void missingProperty(Path path, JsonNode value) { 44 | for (final var asserter : missingPropertyAsserters) { 45 | if (asserter.done(path, value)) { 46 | return; 47 | } 48 | } 49 | throw new AssertionError("Non expected not found property " + path); 50 | } 51 | 52 | @Override 53 | public void extraProperty(Path path, JsonNode extraReceivedValue) { 54 | for (final var asserter: extraPropertyAsserters) { 55 | if (asserter.done(path)) { 56 | return; 57 | } 58 | } 59 | throw new AssertionError("Non expected extra property " + path); 60 | } 61 | 62 | @Override 63 | public void primaryNonMatching(Path path, JsonNode expected, JsonNode value) { 64 | for (final var asserter : primaryNonMatchingAsserters) { 65 | if (asserter.done(path)) { 66 | return; 67 | } 68 | } 69 | throw new AssertionError("Non expected non matching primary property " + path); 70 | } 71 | 72 | @Override 73 | public void primaryMatching(Path path, JsonNode value) { 74 | for (final var asserter : primaryMatchingAsserters) { 75 | if (asserter.done(path)) { 76 | return; 77 | } 78 | } 79 | throw new AssertionError("Non expected matching primary " + path); 80 | } 81 | 82 | public void validate(JsonDiff jsonDiff) { 83 | if (this.expectedSimilarityRate != null && Math.abs(this.expectedSimilarityRate - jsonDiff.similarityRate()) > 0.1) { 84 | throw new AssertionError( 85 | String.format( 86 | "The similarity rate should be equals to \"%s\" but actual value is \"%s\"", 87 | this.expectedSimilarityRate, jsonDiff.similarityRate() 88 | ) 89 | ); 90 | } 91 | jsonDiff.display(this); 92 | 93 | final var allErrors = Stream.of(primaryMatchingAsserters, missingPropertyAsserters, nonMatchingPropertyAsserters, matchingPropertyAsserters, primaryNonMatchingAsserters, extraPropertyAsserters) 94 | .flatMap(Collection::stream) 95 | .filter(asserter -> !asserter.isDone()) 96 | .map(Asserter::getError) 97 | .collect(Collectors.joining("\n")); 98 | 99 | if (!allErrors.isEmpty()) { 100 | throw new AssertionError(allErrors); 101 | } 102 | } 103 | 104 | public JsonDiffAsserter assertNonMatchingProperty(Path path) { 105 | this.nonMatchingPropertyAsserters.add(new NonMatchingPropertyAsserter(path)); 106 | return this; 107 | } 108 | 109 | public JsonDiffAsserter assertMissingProperty(Path path) { 110 | this.missingPropertyAsserters.add(new NotFoundAsserter(path)); 111 | return this; 112 | } 113 | 114 | public JsonDiffAsserter assertMatchingProperty(Path path) { 115 | this.matchingPropertyAsserters.add(new MatchingPropertyAsserter(path)); 116 | return this; 117 | } 118 | 119 | public JsonDiffAsserter assertPrimaryNonMatching(Path path) { 120 | this.primaryNonMatchingAsserters.add(new PrimaryNonMatchingAsserter(path)); 121 | return this; 122 | } 123 | 124 | public JsonDiffAsserter assertPrimaryMatching(Path path) { 125 | this.primaryMatchingAsserters.add(new PrimaryMatchingAsserter(path)); 126 | return this; 127 | } 128 | 129 | public JsonDiffAsserter assertExtraProperty(Path path) { 130 | this.extraPropertyAsserters.add(new ExtraPropertyAsserter(path)); 131 | return this; 132 | } 133 | 134 | public JsonDiffAsserter assertSimilarityRate(double structural, double value) { 135 | return assertSimilarityRate(structural + value); 136 | } 137 | 138 | public JsonDiffAsserter assertSimilarityRate(double value) { 139 | this.expectedSimilarityRate = value; 140 | return this; 141 | } 142 | 143 | interface Asserter { 144 | boolean isDone(); 145 | String getError(); 146 | } 147 | 148 | class ExtraPropertyAsserter implements Asserter { 149 | private final Path path; 150 | private boolean done = false; 151 | 152 | public ExtraPropertyAsserter(Path path) { 153 | this.path = path; 154 | } 155 | 156 | public boolean done(Path path) { 157 | if (this.path.equals(path)) { 158 | this.done = true; 159 | return true; 160 | } 161 | return false; 162 | } 163 | 164 | @Override 165 | public boolean isDone() { 166 | return this.done; 167 | } 168 | 169 | @Override 170 | public String getError() { 171 | return "Expected an extra property " + path; 172 | } 173 | } 174 | 175 | class NotFoundAsserter implements Asserter { 176 | private final Path path; 177 | private boolean done = false; 178 | 179 | public NotFoundAsserter(Path path) { 180 | this.path = path; 181 | } 182 | 183 | public boolean done(Path path, JsonNode value) { 184 | if (this.path.equals(path)) { 185 | this.done = true; 186 | return true; 187 | } 188 | return false; 189 | } 190 | 191 | @Override 192 | public boolean isDone() { 193 | return this.done; 194 | } 195 | 196 | @Override 197 | public String getError() { 198 | return "Expected a not found property " + path; 199 | } 200 | } 201 | 202 | class NonMatchingPropertyAsserter implements Asserter { 203 | private final Path path; 204 | private boolean done = false; 205 | 206 | public NonMatchingPropertyAsserter(Path path) { 207 | this.path = path; 208 | } 209 | 210 | public boolean done(Path path) { 211 | if (this.path.equals(path)) { 212 | this.done = true; 213 | return true; 214 | } 215 | return false; 216 | } 217 | 218 | @Override 219 | public boolean isDone() { 220 | return this.done; 221 | } 222 | 223 | @Override 224 | public String getError() { 225 | return "Expected a non matching property " + path; 226 | } 227 | } 228 | 229 | class MatchingPropertyAsserter implements Asserter { 230 | private final Path path; 231 | private boolean done = false; 232 | 233 | public MatchingPropertyAsserter(Path path) { 234 | this.path = path; 235 | } 236 | 237 | public boolean done(Path path) { 238 | if (this.path.equals(path)) { 239 | this.done = true; 240 | return true; 241 | } 242 | return false; 243 | } 244 | 245 | @Override 246 | public boolean isDone() { 247 | return this.done; 248 | } 249 | 250 | @Override 251 | public String getError() { 252 | return "Expected a matching property " + path; 253 | } 254 | } 255 | 256 | class PrimaryNonMatchingAsserter implements Asserter { 257 | private final Path path; 258 | private boolean done = false; 259 | 260 | public PrimaryNonMatchingAsserter(Path path) { 261 | this.path = path; 262 | } 263 | 264 | public boolean done(Path path) { 265 | if (this.path.equals(path)) { 266 | this.done = true; 267 | return true; 268 | } 269 | return false; 270 | } 271 | 272 | @Override 273 | public boolean isDone() { 274 | return this.done; 275 | } 276 | 277 | @Override 278 | public String getError() { 279 | return "Expected a primary non matching property " + path; 280 | } 281 | } 282 | 283 | class PrimaryMatchingAsserter implements Asserter { 284 | private final Path path; 285 | private boolean done = false; 286 | 287 | public PrimaryMatchingAsserter(Path path) { 288 | this.path = path; 289 | } 290 | 291 | public boolean done(Path path) { 292 | if (this.path.equals(path)) { 293 | this.done = true; 294 | return true; 295 | } 296 | return false; 297 | } 298 | 299 | @Override 300 | public boolean isDone() { 301 | return this.done; 302 | } 303 | 304 | @Override 305 | public String getError() { 306 | return "Expected a primary matching property " + path; 307 | } 308 | } 309 | 310 | } 311 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/matcher/LenientJsonArrayPartialMatcherTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.viewer.JsonDiffViewer; 5 | import com.fasterxml.jackson.databind.node.ArrayNode; 6 | import com.fasterxml.jackson.databind.node.TextNode; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mockito; 9 | 10 | import java.util.List; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.mockito.ArgumentMatchers.any; 14 | 15 | class LenientJsonArrayPartialMatcherTest { 16 | private final static Path path = Path.ROOT.add(Path.PathItem.of("a")); 17 | 18 | @Test 19 | void shouldReturnFullMatchWhenAllItemsAreFound() { 20 | final var array1 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 21 | final var array2 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 22 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 23 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 24 | 25 | final var result = new LenientJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 26 | 27 | assertEquals(path, result.path()); 28 | new JsonDiffAsserter() 29 | .assertSimilarityRate(100) 30 | .assertMatchingProperty(path.add(Path.PathItem.of(0))) 31 | .assertMatchingProperty(path.add(Path.PathItem.of(1))) 32 | .validate(result); 33 | } 34 | 35 | @Test 36 | void shouldReturnFullMatchWhenAllItemsAreFoundWithBadOrder() { 37 | final var array1 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 38 | final var array2 = new ArrayNode(null, List.of(TextNode.valueOf("b"), TextNode.valueOf("a"))); 39 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 40 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 41 | 42 | final var result = new LenientJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 43 | 44 | assertEquals(path, result.path()); 45 | new JsonDiffAsserter() 46 | .assertSimilarityRate(100) 47 | .assertMatchingProperty(path.add(Path.PathItem.of(0))) 48 | .assertMatchingProperty(path.add(Path.PathItem.of(1))) 49 | .validate(result); 50 | } 51 | 52 | @Test 53 | void shouldReturnFullMatchForEmptyArray() { 54 | final var array1 = new ArrayNode(null, List.of()); 55 | final var array2 = new ArrayNode(null, List.of()); 56 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 57 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 58 | 59 | final var result = new LenientJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 60 | 61 | assertEquals(path, result.path()); 62 | new JsonDiffAsserter() 63 | .assertSimilarityRate(100) 64 | .validate(result); 65 | } 66 | 67 | @Test 68 | void shouldReturnNoMatchWhenSameNumberItemWithNoMatch() { 69 | final var array1 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 70 | final var array2 = new ArrayNode(null, List.of(TextNode.valueOf("c"), TextNode.valueOf("d"))); 71 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 72 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 73 | 74 | final var result = new LenientJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 75 | 76 | assertEquals(path, result.path()); 77 | new JsonDiffAsserter() 78 | .assertSimilarityRate(0) 79 | .assertNonMatchingProperty(path.add(Path.PathItem.of(0))) 80 | .assertNonMatchingProperty(path.add(Path.PathItem.of(1))) 81 | .validate(result); 82 | } 83 | 84 | @Test 85 | void shouldReturnPartialMatchWhenMissingItem() { 86 | final var array1 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 87 | final var array2 = new ArrayNode(null, List.of(TextNode.valueOf("b"))); 88 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 89 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 90 | 91 | final var result = new LenientJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 92 | 93 | assertEquals(path, result.path()); 94 | new JsonDiffAsserter() 95 | .assertSimilarityRate(50) 96 | .assertMissingProperty(path.add(Path.PathItem.of(0))) 97 | .assertMatchingProperty(path.add(Path.PathItem.of(1))) 98 | .validate(result); 99 | } 100 | 101 | @Test 102 | void shouldReturnPartialMatchWhenExtraItems() { 103 | final var array1 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("c"))); 104 | final var array2 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"), TextNode.valueOf("c"), TextNode.valueOf("d"))); 105 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 106 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 107 | 108 | final var result = new LenientJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 109 | 110 | assertEquals(path, result.path()); 111 | new JsonDiffAsserter() 112 | .assertSimilarityRate(50) 113 | .assertMatchingProperty(path.add(Path.PathItem.of(0))) 114 | .assertMatchingProperty(path.add(Path.PathItem.of(1))) // the path of matching prop is the path on the expected object 115 | .assertExtraProperty(path.add(Path.PathItem.of(1))) // the path of extra property is the path on the received object not on the expected 116 | .assertExtraProperty(path.add(Path.PathItem.of(3))) 117 | .validate(result); 118 | } 119 | 120 | @Test 121 | void shouldWorkWithDuplicatedArrayItemsOnExpected() { 122 | final var expected = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("a"), TextNode.valueOf("b"))); 123 | final var actual = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 124 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 125 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 126 | 127 | final var result = new LenientJsonArrayPartialMatcher().jsonDiff(path, expected, actual, jsonMatcher); 128 | 129 | assertEquals(path, result.path()); 130 | new JsonDiffAsserter() 131 | .assertSimilarityRate(66.66) 132 | .assertMatchingProperty(path.add(Path.PathItem.of(0))) 133 | .assertMissingProperty(path.add(Path.PathItem.of(1))) 134 | .assertMatchingProperty(path.add(Path.PathItem.of(2))) 135 | .validate(result); 136 | } 137 | 138 | @Test 139 | void shouldWorkWithDuplicatedArrayItemsOnActual() { 140 | final var expected = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 141 | final var actual = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("a"), TextNode.valueOf("b"))); 142 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 143 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 144 | 145 | final var result = new LenientJsonArrayPartialMatcher().jsonDiff(path, expected, actual, jsonMatcher); 146 | 147 | assertEquals(path, result.path()); 148 | new JsonDiffAsserter() 149 | .assertSimilarityRate(66.66) 150 | .assertMatchingProperty(path.add(Path.PathItem.of(0))) 151 | .assertExtraProperty(path.add(Path.PathItem.of(1))) 152 | .assertMatchingProperty(path.add(Path.PathItem.of(1))) 153 | .validate(result); 154 | } 155 | 156 | private JsonDiff matchByEquality(org.mockito.invocation.InvocationOnMock args) { 157 | if (args.getArgument(1).equals(args.getArgument(2))) { 158 | return fullMatchJsonDiff(args.getArgument(0)); 159 | } else { 160 | return nonMatchJsonDiff(args.getArgument(0)); 161 | } 162 | } 163 | 164 | private JsonDiff fullMatchJsonDiff(Path path) { 165 | return new JsonDiff() { 166 | @Override 167 | public double similarityRate() { 168 | return 100; 169 | } 170 | 171 | @Override 172 | public void display(JsonDiffViewer viewer) { 173 | 174 | } 175 | 176 | @Override 177 | public Path path() { 178 | return path; 179 | } 180 | }; 181 | } 182 | 183 | private JsonDiff nonMatchJsonDiff(Path path) { 184 | return new JsonDiff() { 185 | @Override 186 | public double similarityRate() { 187 | return 0; 188 | } 189 | 190 | @Override 191 | public void display(JsonDiffViewer viewer) { 192 | 193 | } 194 | 195 | @Override 196 | public Path path() { 197 | return path; 198 | } 199 | }; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/matcher/LenientJsonObjectPartialMatcherTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.viewer.JsonDiffViewer; 5 | import com.fasterxml.jackson.databind.node.ObjectNode; 6 | import com.fasterxml.jackson.databind.node.TextNode; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mockito; 9 | 10 | import java.util.Map; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.mockito.ArgumentMatchers.any; 14 | 15 | class LenientJsonObjectPartialMatcherTest { 16 | private final Path path = Path.ROOT.add(Path.PathItem.of("foo")); 17 | 18 | @Test 19 | void shouldReturnFullMatchForTwoEmptyObjects() { 20 | final var object1 = new ObjectNode(null); 21 | final var object2 = new ObjectNode(null); 22 | 23 | final var result = new LenientJsonObjectPartialMatcher().jsonDiff(path, object1, object2, null); 24 | 25 | assertEquals(path, result.path()); 26 | new JsonDiffAsserter() 27 | .assertSimilarityRate(60, 40) 28 | .assertPrimaryMatching(path) 29 | .validate(result); 30 | } 31 | 32 | @Test 33 | void shouldReturnNonMachIfAllPropertiesAreNotFound() { 34 | final var object1 = new ObjectNode(null, Map.of( 35 | "a", TextNode.valueOf("a"), 36 | "b", TextNode.valueOf("b") 37 | )); 38 | final var object2 = new ObjectNode(null); 39 | 40 | final var result = new LenientJsonObjectPartialMatcher().jsonDiff(path, object1, object2, null); 41 | 42 | assertEquals(path, result.path()); 43 | new JsonDiffAsserter() 44 | .assertSimilarityRate(0, 0) 45 | .assertMissingProperty(path.add(Path.PathItem.of("a"))) 46 | .assertMissingProperty(path.add(Path.PathItem.of("b"))) 47 | .validate(result); 48 | } 49 | 50 | @Test 51 | void shouldReturnNonMatchingPropertyIfAllPropertiesAreFoundWithoutMatch() { 52 | final var object1 = new ObjectNode(null, Map.of( 53 | "a", TextNode.valueOf("a"), 54 | "b", TextNode.valueOf("b") 55 | )); 56 | final var object2 = new ObjectNode(null, Map.of( 57 | "a", TextNode.valueOf("c"), 58 | "b", TextNode.valueOf("d") 59 | )); 60 | final var parentMatcher = Mockito.mock(JsonMatcher.class); 61 | Mockito.when(parentMatcher.diff(any(), any(), any())).thenAnswer((args) -> nonMatchJsonDiff(args.getArgument(0))); 62 | final var result = new LenientJsonObjectPartialMatcher().jsonDiff(path, object1, object2, parentMatcher); 63 | 64 | assertEquals(path, result.path()); 65 | new JsonDiffAsserter() 66 | .assertSimilarityRate(60, 0) 67 | .assertNonMatchingProperty(path.add(Path.PathItem.of("a"))) 68 | .assertNonMatchingProperty(path.add(Path.PathItem.of("b"))) 69 | .validate(result); 70 | } 71 | 72 | @Test 73 | void shouldMixMatchingAndNotFoundPropertiesOnSameResult() { 74 | final var object1 = new ObjectNode(null, Map.of( 75 | "a", TextNode.valueOf("a"), 76 | "b", TextNode.valueOf("b") 77 | )); 78 | final var object2 = new ObjectNode(null, Map.of( 79 | "a", TextNode.valueOf("a"), 80 | "c", TextNode.valueOf("b") 81 | )); 82 | final var parentMatcher = Mockito.mock(JsonMatcher.class); 83 | Mockito.when(parentMatcher.diff(any(), any(), any())).thenAnswer((args) -> fullMatchJsonDiff(args.getArgument(0))); 84 | final var result = new LenientJsonObjectPartialMatcher().jsonDiff(path, object1, object2, parentMatcher); 85 | 86 | assertEquals(path, result.path()); 87 | new JsonDiffAsserter() 88 | .assertSimilarityRate(30, 20) 89 | .assertMatchingProperty(path.add(Path.PathItem.of("a"))) 90 | .assertMissingProperty(path.add(Path.PathItem.of("b"))) 91 | .validate(result); 92 | } 93 | 94 | @Test 95 | void shouldReturnFullMatchingPropertyAllPropertiesAreFoundAndMatch() { 96 | final var object1 = new ObjectNode(null, Map.of( 97 | "a", TextNode.valueOf("a"), 98 | "b", TextNode.valueOf("b") 99 | )); 100 | final var object2 = new ObjectNode(null, Map.of( 101 | "a", TextNode.valueOf("a"), 102 | "b", TextNode.valueOf("b") 103 | )); 104 | final var parentMatcher = Mockito.mock(JsonMatcher.class); 105 | Mockito.when(parentMatcher.diff(any(), any(), any())).thenAnswer((args) -> fullMatchJsonDiff(args.getArgument(0))); 106 | final var result = new LenientJsonObjectPartialMatcher().jsonDiff(path, object1, object2, parentMatcher); 107 | 108 | assertEquals(path, result.path()); 109 | new JsonDiffAsserter() 110 | .assertSimilarityRate(60, 40) 111 | .assertMatchingProperty(path.add(Path.PathItem.of("a"))) 112 | .assertMatchingProperty(path.add(Path.PathItem.of("b"))) 113 | .validate(result); 114 | } 115 | 116 | @Test 117 | void shouldReturnSimilarityIfOnlyOneProperty() { 118 | final var object1 = new ObjectNode(null, Map.of( 119 | "a", TextNode.valueOf("a"), 120 | "b", TextNode.valueOf("b"), 121 | "c", TextNode.valueOf("b") 122 | )); 123 | final var object2 = new ObjectNode(null, Map.of( 124 | "a", TextNode.valueOf("a") 125 | )); 126 | final var parentMatcher = Mockito.mock(JsonMatcher.class); 127 | Mockito.when(parentMatcher.diff(any(), any(), any())).thenAnswer((args) -> fullMatchJsonDiff(args.getArgument(0))); 128 | final var result = new LenientJsonObjectPartialMatcher().jsonDiff(path, object1, object2, parentMatcher); 129 | 130 | assertEquals(path, result.path()); 131 | new JsonDiffAsserter() 132 | .assertSimilarityRate(20, 40.0 / 3.0) 133 | .assertMatchingProperty(path.add(Path.PathItem.of("a"))) 134 | .assertMissingProperty(path.add(Path.PathItem.of("b"))) 135 | .assertMissingProperty(path.add(Path.PathItem.of("c"))) 136 | .validate(result); 137 | } 138 | 139 | private JsonDiff nonMatchJsonDiff(Path path) { 140 | return new JsonDiff() { 141 | @Override 142 | public double similarityRate() { 143 | return 0; 144 | } 145 | 146 | @Override 147 | public void display(JsonDiffViewer viewer) { 148 | 149 | } 150 | 151 | @Override 152 | public Path path() { 153 | return path; 154 | } 155 | }; 156 | } 157 | 158 | private JsonDiff fullMatchJsonDiff(Path path) { 159 | return new JsonDiff() { 160 | @Override 161 | public double similarityRate() { 162 | return 100; 163 | } 164 | 165 | @Override 166 | public void display(JsonDiffViewer viewer) { 167 | 168 | } 169 | 170 | @Override 171 | public Path path() { 172 | return path; 173 | } 174 | }; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcherTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.fasterxml.jackson.databind.node.DecimalNode; 5 | import com.fasterxml.jackson.databind.node.IntNode; 6 | import com.fasterxml.jackson.databind.node.TextNode; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mockito; 9 | 10 | import java.math.BigDecimal; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | public class LenientNumberPrimitivePartialMatcherTest { 15 | private final static Path expectedPath = Path.ROOT.add(Path.PathItem.of("property")); 16 | 17 | @Test 18 | public void shouldReturnAMatchIfNodeAreStrictEqualsNumbers() { 19 | final var number1 = DecimalNode.valueOf(BigDecimal.valueOf(101, 1)); 20 | final var number2 = DecimalNode.valueOf(BigDecimal.valueOf(101, 1)); 21 | 22 | final var jsonDiff = new LenientNumberPrimitivePartialMatcher(null) 23 | .jsonDiff(expectedPath, number1, number2, Mockito.mock(JsonMatcher.class)); 24 | 25 | assertEquals(100, jsonDiff.similarityRate()); 26 | assertEquals(expectedPath, jsonDiff.path()); 27 | new JsonDiffAsserter() 28 | .assertPrimaryMatching(expectedPath) 29 | .validate(jsonDiff); 30 | } 31 | 32 | @Test 33 | public void shouldReturnAMatchIfNodeAreEqualsNumbersWithDifferentType() { 34 | final var number1 = IntNode.valueOf(100); 35 | final var number2 = DecimalNode.valueOf(BigDecimal.valueOf(1000, 1)); 36 | 37 | final var jsonDiff = new LenientNumberPrimitivePartialMatcher(null) 38 | .jsonDiff(expectedPath, number1, number2, Mockito.mock(JsonMatcher.class)); 39 | 40 | assertEquals(100, jsonDiff.similarityRate()); 41 | assertEquals(expectedPath, jsonDiff.path()); 42 | new JsonDiffAsserter() 43 | .assertPrimaryMatching(expectedPath) 44 | .validate(jsonDiff); 45 | } 46 | 47 | @Test 48 | public void shouldReturnANonMatchIfNodeAreEqualsNumbersWithDifferentDecimalValue() { 49 | final var number1 = DecimalNode.valueOf(BigDecimal.valueOf(1000, 1)); 50 | final var number2 = DecimalNode.valueOf(BigDecimal.valueOf(1001, 1)); 51 | 52 | final var jsonDiff = new LenientNumberPrimitivePartialMatcher(null) 53 | .jsonDiff(expectedPath, number1, number2, Mockito.mock(JsonMatcher.class)); 54 | 55 | assertEquals(0, jsonDiff.similarityRate()); 56 | assertEquals(expectedPath, jsonDiff.path()); 57 | new JsonDiffAsserter() 58 | .assertPrimaryNonMatching(expectedPath) 59 | .validate(jsonDiff); 60 | } 61 | 62 | @Test 63 | public void shouldReturnANonMatchIfNodeAreEqualsNumbersWithDifferentIntValue() { 64 | final var number1 = DecimalNode.valueOf(BigDecimal.valueOf(1000, 1)); 65 | final var number2 = DecimalNode.valueOf(BigDecimal.valueOf(2000, 1)); 66 | 67 | final var jsonDiff = new LenientNumberPrimitivePartialMatcher(null) 68 | .jsonDiff(expectedPath, number1, number2, Mockito.mock(JsonMatcher.class)); 69 | 70 | assertEquals(0, jsonDiff.similarityRate()); 71 | assertEquals(expectedPath, jsonDiff.path()); 72 | new JsonDiffAsserter() 73 | .assertPrimaryNonMatching(expectedPath) 74 | .validate(jsonDiff); 75 | } 76 | 77 | @Test 78 | public void shouldCallTheDelegatedIfNodeHaveDifferentType() { 79 | final var value1 = IntNode.valueOf(100); 80 | final var value2 = TextNode.valueOf("100"); 81 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 82 | final var delegated = Mockito.mock(PartialJsonMatcher.class); 83 | final var expectedJsonDiff = Mockito.mock(JsonDiff.class); 84 | Mockito.when(delegated.jsonDiff(expectedPath, value1, value2, jsonMatcher)).thenReturn(expectedJsonDiff); 85 | 86 | final var jsonDiff = new LenientNumberPrimitivePartialMatcher(delegated) 87 | .jsonDiff(expectedPath, value1, value2, jsonMatcher); 88 | 89 | assertEquals(expectedJsonDiff, jsonDiff); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/matcher/PathTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | public class PathTest { 8 | 9 | @Test 10 | public void rootShouldBeDisplayedAs$() { 11 | final var result = new Path(); 12 | 13 | assertEquals("$", result.toString()); 14 | } 15 | 16 | @Test 17 | public void canAddPathToRoot() { 18 | final var result = new Path().add(Path.PathItem.of("property")); 19 | 20 | assertEquals("$.property", result.toString()); 21 | } 22 | 23 | @Test 24 | public void canAddPathToPath() { 25 | final var result = new Path().add(Path.PathItem.of("property")).add(Path.PathItem.of("property2")); 26 | 27 | assertEquals("$.property.property2", result.toString()); 28 | } 29 | 30 | @Test 31 | public void canAddArrayIndex() { 32 | final var result = new Path().add(Path.PathItem.of("property")).add(Path.PathItem.of(1)); 33 | 34 | assertEquals("$.property.1", result.toString()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/matcher/StrictJsonArrayPartialMatcherTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.viewer.JsonDiffViewer; 5 | import com.fasterxml.jackson.databind.node.ArrayNode; 6 | import com.fasterxml.jackson.databind.node.TextNode; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mockito; 9 | 10 | import java.util.List; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.mockito.ArgumentMatchers.any; 14 | 15 | class StrictJsonArrayPartialMatcherTest { 16 | private final static Path path = Path.ROOT.add(Path.PathItem.of("a")); 17 | 18 | @Test 19 | void shouldReturnFullMatchWhenAllItemsAreFound() { 20 | final var array1 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 21 | final var array2 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 22 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 23 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 24 | 25 | final var result = new StrictJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 26 | 27 | assertEquals(path, result.path()); 28 | new JsonDiffAsserter() 29 | .assertSimilarityRate(100) 30 | .assertMatchingProperty(path.add(Path.PathItem.of(0))) 31 | .assertMatchingProperty(path.add(Path.PathItem.of(1))) 32 | .validate(result); 33 | } 34 | 35 | @Test 36 | void shouldReturnFullMatchForEmptyArray() { 37 | final var array1 = new ArrayNode(null, List.of()); 38 | final var array2 = new ArrayNode(null, List.of()); 39 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 40 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 41 | 42 | final var result = new StrictJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 43 | 44 | assertEquals(path, result.path()); 45 | new JsonDiffAsserter() 46 | .assertSimilarityRate(100) 47 | .validate(result); 48 | } 49 | 50 | @Test 51 | void shouldReturnNoMatchWhenItemsAreNonOrdered() { 52 | final var array1 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 53 | final var array2 = new ArrayNode(null, List.of(TextNode.valueOf("b"), TextNode.valueOf("a"))); 54 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 55 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 56 | 57 | final var result = new StrictJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 58 | 59 | assertEquals(path, result.path()); 60 | new JsonDiffAsserter() 61 | .assertSimilarityRate(0) 62 | .assertNonMatchingProperty(path.add(Path.PathItem.of(0))) 63 | .assertNonMatchingProperty(path.add(Path.PathItem.of(1))) 64 | .validate(result); 65 | } 66 | 67 | @Test 68 | void shouldReturnNoMatchWhenSameNumberItemWithNoMatch() { 69 | final var array1 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 70 | final var array2 = new ArrayNode(null, List.of(TextNode.valueOf("c"), TextNode.valueOf("d"))); 71 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 72 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 73 | 74 | final var result = new StrictJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 75 | 76 | assertEquals(0, result.similarityRate()); 77 | assertEquals(path, result.path()); 78 | new JsonDiffAsserter() 79 | .assertNonMatchingProperty(path.add(Path.PathItem.of(0))) 80 | .assertNonMatchingProperty(path.add(Path.PathItem.of(1))) 81 | .validate(result); 82 | } 83 | 84 | @Test 85 | void shouldReturnPartialMatchWhenMissingItem() { 86 | final var array1 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"))); 87 | final var array2 = new ArrayNode(null, List.of(TextNode.valueOf("a"))); 88 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 89 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 90 | 91 | final var result = new StrictJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 92 | 93 | assertEquals(path, result.path()); 94 | new JsonDiffAsserter() 95 | .assertSimilarityRate(50) 96 | .assertMissingProperty(path.add(Path.PathItem.of(1))) 97 | .assertMatchingProperty(path.add(Path.PathItem.of(0))) 98 | .validate(result); 99 | } 100 | 101 | @Test 102 | void shouldReturnPartialMatchWhenExtraItems() { 103 | final var array1 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("c"))); 104 | final var array2 = new ArrayNode(null, List.of(TextNode.valueOf("a"), TextNode.valueOf("b"), TextNode.valueOf("c"))); 105 | final var jsonMatcher = Mockito.mock(JsonMatcher.class); 106 | Mockito.when(jsonMatcher.diff(any(), any(), any())).thenAnswer(this::matchByEquality); 107 | 108 | final var result = new StrictJsonArrayPartialMatcher().jsonDiff(path, array1, array2, jsonMatcher); 109 | 110 | assertEquals(path, result.path()); 111 | new JsonDiffAsserter() 112 | .assertSimilarityRate(33.33d) 113 | .assertMatchingProperty(path.add(Path.PathItem.of(0))) 114 | .assertNonMatchingProperty(path.add(Path.PathItem.of(1))) 115 | .assertExtraProperty(path.add(Path.PathItem.of(2))) 116 | .validate(result); 117 | } 118 | 119 | private JsonDiff matchByEquality(org.mockito.invocation.InvocationOnMock args) { 120 | if (args.getArgument(1).equals(args.getArgument(2))) { 121 | return fullMatchJsonDiff(args.getArgument(0)); 122 | } else { 123 | return nonMatchJsonDiff(args.getArgument(0)); 124 | } 125 | } 126 | 127 | private JsonDiff fullMatchJsonDiff(Path path) { 128 | return new JsonDiff() { 129 | @Override 130 | public double similarityRate() { 131 | return 100; 132 | } 133 | 134 | @Override 135 | public void display(JsonDiffViewer viewer) { 136 | 137 | } 138 | 139 | @Override 140 | public Path path() { 141 | return path; 142 | } 143 | }; 144 | } 145 | 146 | private JsonDiff nonMatchJsonDiff(Path path) { 147 | return new JsonDiff() { 148 | @Override 149 | public double similarityRate() { 150 | return 0; 151 | } 152 | 153 | @Override 154 | public void display(JsonDiffViewer viewer) { 155 | 156 | } 157 | 158 | @Override 159 | public Path path() { 160 | return path; 161 | } 162 | }; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/matcher/StrictJsonObjectPartialMatcherTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.deblock.jsondiff.diff.JsonDiff; 4 | import com.deblock.jsondiff.viewer.JsonDiffViewer; 5 | import com.fasterxml.jackson.databind.node.ObjectNode; 6 | import com.fasterxml.jackson.databind.node.TextNode; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mockito; 9 | 10 | import java.util.Map; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.mockito.ArgumentMatchers.any; 14 | 15 | class StrictJsonObjectPartialMatcherTest { 16 | private final Path path = Path.ROOT.add(Path.PathItem.of("foo")); 17 | 18 | @Test 19 | void shouldReturnFullMatchForTwoEmptyObjects() { 20 | final var object1 = new ObjectNode(null); 21 | final var object2 = new ObjectNode(null); 22 | 23 | final var result = new StrictJsonObjectPartialMatcher().jsonDiff(path, object1, object2, null); 24 | 25 | assertEquals(100, result.similarityRate()); 26 | assertEquals(path, result.path()); 27 | } 28 | 29 | @Test 30 | void shouldReturnNonMachIfAllPropertiesAreNotFound() { 31 | final var object1 = new ObjectNode(null, Map.of( 32 | "a", TextNode.valueOf("a"), 33 | "b", TextNode.valueOf("b") 34 | )); 35 | final var object2 = new ObjectNode(null); 36 | 37 | final var result = new StrictJsonObjectPartialMatcher().jsonDiff(path, object1, object2, null); 38 | 39 | assertEquals(path, result.path()); 40 | new JsonDiffAsserter() 41 | .assertSimilarityRate(0, 0) 42 | .assertMissingProperty(path.add(Path.PathItem.of("a"))) 43 | .assertMissingProperty(path.add(Path.PathItem.of("b"))) 44 | .validate(result); 45 | } 46 | 47 | @Test 48 | void shouldReturnNonMatchingPropertyIfAllPropertiesAreFoundWithoutMatch() { 49 | final var object1 = new ObjectNode(null, Map.of( 50 | "a", TextNode.valueOf("a"), 51 | "b", TextNode.valueOf("b") 52 | )); 53 | final var object2 = new ObjectNode(null, Map.of( 54 | "a", TextNode.valueOf("c"), 55 | "b", TextNode.valueOf("d") 56 | )); 57 | final var parentMatcher = Mockito.mock(JsonMatcher.class); 58 | Mockito.when(parentMatcher.diff(any(), any(), any())).thenAnswer((args) -> nonMatchJsonDiff(args.getArgument(0))); 59 | final var result = new StrictJsonObjectPartialMatcher().jsonDiff(path, object1, object2, parentMatcher); 60 | 61 | assertEquals(path, result.path()); 62 | new JsonDiffAsserter() 63 | .assertSimilarityRate(60, 0) 64 | .assertNonMatchingProperty(path.add(Path.PathItem.of("a"))) 65 | .assertNonMatchingProperty(path.add(Path.PathItem.of("b"))) 66 | .validate(result); 67 | } 68 | 69 | @Test 70 | void shouldMixMatchingAndNotFoundPropertiesOnSameResult() { 71 | final var object1 = new ObjectNode(null, Map.of( 72 | "a", TextNode.valueOf("a"), 73 | "b", TextNode.valueOf("b") 74 | )); 75 | final var object2 = new ObjectNode(null, Map.of( 76 | "a", TextNode.valueOf("a"), 77 | "c", TextNode.valueOf("b") 78 | )); 79 | final var parentMatcher = Mockito.mock(JsonMatcher.class); 80 | Mockito.when(parentMatcher.diff(any(), any(), any())).thenAnswer((args) -> fullMatchJsonDiff(args.getArgument(0))); 81 | final var result = new StrictJsonObjectPartialMatcher().jsonDiff(path, object1, object2, parentMatcher); 82 | 83 | assertEquals(path, result.path()); 84 | new JsonDiffAsserter() 85 | .assertSimilarityRate(20, 13.33) 86 | .assertMatchingProperty(path.add(Path.PathItem.of("a"))) 87 | .assertMissingProperty(path.add(Path.PathItem.of("b"))) 88 | .assertExtraProperty(path.add(Path.PathItem.of("c"))) 89 | .validate(result); 90 | } 91 | 92 | @Test 93 | void shouldReturnFullMatchingPropertyAllPropertiesAreFoundAndMatch() { 94 | final var object1 = new ObjectNode(null, Map.of( 95 | "a", TextNode.valueOf("a"), 96 | "b", TextNode.valueOf("b") 97 | )); 98 | final var object2 = new ObjectNode(null, Map.of( 99 | "a", TextNode.valueOf("a"), 100 | "b", TextNode.valueOf("b") 101 | )); 102 | final var parentMatcher = Mockito.mock(JsonMatcher.class); 103 | Mockito.when(parentMatcher.diff(any(), any(), any())).thenAnswer((args) -> fullMatchJsonDiff(args.getArgument(0))); 104 | final var result = new StrictJsonObjectPartialMatcher().jsonDiff(path, object1, object2, parentMatcher); 105 | 106 | assertEquals(path, result.path()); 107 | new JsonDiffAsserter() 108 | .assertSimilarityRate(60, 40) 109 | .assertMatchingProperty(path.add(Path.PathItem.of("a"))) 110 | .assertMatchingProperty(path.add(Path.PathItem.of("b"))) 111 | .validate(result); 112 | } 113 | 114 | private JsonDiff nonMatchJsonDiff(Path path) { 115 | return new JsonDiff() { 116 | @Override 117 | public double similarityRate() { 118 | return 0; 119 | } 120 | 121 | @Override 122 | public void display(JsonDiffViewer viewer) { 123 | 124 | } 125 | 126 | @Override 127 | public Path path() { 128 | return path; 129 | } 130 | }; 131 | } 132 | 133 | private JsonDiff fullMatchJsonDiff(Path path) { 134 | return new JsonDiff() { 135 | @Override 136 | public double similarityRate() { 137 | return 100; 138 | } 139 | 140 | @Override 141 | public void display(JsonDiffViewer viewer) { 142 | 143 | } 144 | 145 | @Override 146 | public Path path() { 147 | return path; 148 | } 149 | }; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/matcher/StrictPrimitivePartialMatcherTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.matcher; 2 | 3 | import com.fasterxml.jackson.databind.node.BooleanNode; 4 | import com.fasterxml.jackson.databind.node.DecimalNode; 5 | import com.fasterxml.jackson.databind.node.IntNode; 6 | import com.fasterxml.jackson.databind.node.TextNode; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mockito; 9 | 10 | import java.math.BigDecimal; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | public class StrictPrimitivePartialMatcherTest { 15 | private final static Path expectedPath = Path.ROOT.add(Path.PathItem.of("property")); 16 | 17 | @Test 18 | public void shouldReturnAFullMatchIfNodeAreEqualsString() { 19 | final var string1 = TextNode.valueOf("a"); 20 | final var string2 = TextNode.valueOf("a"); 21 | 22 | final var jsonDiff = new StrictPrimitivePartialMatcher().jsonDiff(expectedPath, string1, string2, Mockito.mock(JsonMatcher.class)); 23 | 24 | assertEquals(100, jsonDiff.similarityRate()); 25 | assertEquals(expectedPath, jsonDiff.path()); 26 | new JsonDiffAsserter() 27 | .assertPrimaryMatching(expectedPath) 28 | .validate(jsonDiff); 29 | } 30 | 31 | @Test 32 | public void shouldReturnANoMatchIfNodeAreNotEqualsString() { 33 | final var string1 = TextNode.valueOf("a"); 34 | final var string2 = TextNode.valueOf("c"); 35 | 36 | final var jsonDiff = new StrictPrimitivePartialMatcher().jsonDiff(expectedPath, string1, string2, Mockito.mock(JsonMatcher.class)); 37 | 38 | assertEquals(0, jsonDiff.similarityRate()); 39 | assertEquals(expectedPath, jsonDiff.path()); 40 | new JsonDiffAsserter() 41 | .assertPrimaryNonMatching(expectedPath) 42 | .validate(jsonDiff); 43 | } 44 | 45 | @Test 46 | public void shouldReturnAMatchIfNodeAreStrictEqualsNumbers() { 47 | final var number1 = DecimalNode.valueOf(BigDecimal.valueOf(101, 1)); 48 | final var number2 = DecimalNode.valueOf(BigDecimal.valueOf(101, 1)); 49 | 50 | final var jsonDiff = new StrictPrimitivePartialMatcher().jsonDiff(expectedPath, number1, number2, Mockito.mock(JsonMatcher.class)); 51 | 52 | assertEquals(100, jsonDiff.similarityRate()); 53 | assertEquals(expectedPath, jsonDiff.path()); 54 | new JsonDiffAsserter() 55 | .assertPrimaryMatching(expectedPath) 56 | .validate(jsonDiff); 57 | } 58 | 59 | @Test 60 | public void shouldReturnANonMatchIfNodeAreEqualsNumbersWithDifferentType() { 61 | final var number1 = IntNode.valueOf(100); 62 | final var number2 = DecimalNode.valueOf(BigDecimal.valueOf(1000, 1)); 63 | 64 | final var jsonDiff = new StrictPrimitivePartialMatcher().jsonDiff(expectedPath, number1, number2, Mockito.mock(JsonMatcher.class)); 65 | 66 | assertEquals(0, jsonDiff.similarityRate()); 67 | assertEquals(expectedPath, jsonDiff.path()); 68 | new JsonDiffAsserter() 69 | .assertPrimaryNonMatching(expectedPath) 70 | .validate(jsonDiff); 71 | } 72 | 73 | @Test 74 | public void shouldReturnANonMatchIfNodeAreEqualsNumbersWithDifferentDecimalValue() { 75 | final var number1 = DecimalNode.valueOf(BigDecimal.valueOf(1000, 1)); 76 | final var number2 = DecimalNode.valueOf(BigDecimal.valueOf(1001, 1)); 77 | 78 | final var jsonDiff = new StrictPrimitivePartialMatcher().jsonDiff(expectedPath, number1, number2, Mockito.mock(JsonMatcher.class)); 79 | 80 | assertEquals(0, jsonDiff.similarityRate()); 81 | assertEquals(expectedPath, jsonDiff.path()); 82 | new JsonDiffAsserter() 83 | .assertPrimaryNonMatching(expectedPath) 84 | .validate(jsonDiff); 85 | } 86 | 87 | @Test 88 | public void shouldReturnANonMatchIfNodeAreEqualsNumbersWithDifferentIntValue() { 89 | final var number1 = DecimalNode.valueOf(BigDecimal.valueOf(1000, 1)); 90 | final var number2 = DecimalNode.valueOf(BigDecimal.valueOf(2000, 1)); 91 | 92 | final var jsonDiff = new StrictPrimitivePartialMatcher().jsonDiff(expectedPath, number1, number2, Mockito.mock(JsonMatcher.class)); 93 | 94 | assertEquals(0, jsonDiff.similarityRate()); 95 | assertEquals(expectedPath, jsonDiff.path()); 96 | new JsonDiffAsserter() 97 | .assertPrimaryNonMatching(expectedPath) 98 | .validate(jsonDiff); 99 | } 100 | 101 | @Test 102 | public void shouldReturnANonMatchIfNodeHaveDifferentType() { 103 | final var value1 = IntNode.valueOf(100); 104 | final var value2 = TextNode.valueOf("100"); 105 | 106 | final var jsonDiff = new StrictPrimitivePartialMatcher().jsonDiff(expectedPath, value1, value2, Mockito.mock(JsonMatcher.class)); 107 | 108 | assertEquals(0, jsonDiff.similarityRate()); 109 | assertEquals(expectedPath, jsonDiff.path()); 110 | new JsonDiffAsserter() 111 | .assertPrimaryNonMatching(expectedPath) 112 | .validate(jsonDiff); 113 | } 114 | 115 | @Test 116 | public void shouldReturnAMatchIfNodeAreSameBoolean() { 117 | final var boolean1 = BooleanNode.valueOf(true); 118 | final var boolean2 = BooleanNode.valueOf(true); 119 | 120 | final var jsonDiff = new StrictPrimitivePartialMatcher().jsonDiff(expectedPath, boolean1, boolean2, Mockito.mock(JsonMatcher.class)); 121 | 122 | assertEquals(100, jsonDiff.similarityRate()); 123 | assertEquals(expectedPath, jsonDiff.path()); 124 | new JsonDiffAsserter() 125 | .assertPrimaryMatching(expectedPath) 126 | .validate(jsonDiff); 127 | } 128 | 129 | @Test 130 | public void shouldReturnANoMatchIfNodeAreSameBoolean() { 131 | final var boolean1 = BooleanNode.valueOf(true); 132 | final var boolean2 = BooleanNode.valueOf(false); 133 | 134 | final var jsonDiff = new StrictPrimitivePartialMatcher().jsonDiff(expectedPath, boolean1, boolean2, Mockito.mock(JsonMatcher.class)); 135 | 136 | assertEquals(0, jsonDiff.similarityRate()); 137 | assertEquals(expectedPath, jsonDiff.path()); 138 | new JsonDiffAsserter() 139 | .assertPrimaryNonMatching(expectedPath) 140 | .validate(jsonDiff); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/viewer/OnlyErrorDiffViewerTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.viewer; 2 | 3 | import com.deblock.jsondiff.diff.JsonObjectDiff; 4 | import com.deblock.jsondiff.diff.MatchedPrimaryDiff; 5 | import com.deblock.jsondiff.diff.UnMatchedPrimaryDiff; 6 | import com.deblock.jsondiff.matcher.Path; 7 | import com.fasterxml.jackson.databind.node.TextNode; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | public class OnlyErrorDiffViewerTest { 13 | private final static Path path = Path.ROOT.add(Path.PathItem.of("a")); 14 | 15 | @Test 16 | public void shouldReturnErrorPath() { 17 | final var viewer = new OnlyErrorDiffViewer(); 18 | final var objectPath = path.add(Path.PathItem.of("b")); 19 | final var jsonObjectDiff = new JsonObjectDiff(objectPath); 20 | jsonObjectDiff.addNotFoundProperty("c", TextNode.valueOf("a")); 21 | 22 | viewer.matchingProperty(path.add(Path.PathItem.of("e")), new MatchedPrimaryDiff(path.add(Path.PathItem.of("e")), TextNode.valueOf("z"))); 23 | viewer.primaryMatching(path.add(Path.PathItem.of("d")), TextNode.valueOf("c")); 24 | viewer.nonMatchingProperty(null, jsonObjectDiff); 25 | viewer.primaryNonMatching(path.add(Path.PathItem.of("c")), TextNode.valueOf("a"), TextNode.valueOf("b")); 26 | viewer.extraProperty(path.add(Path.PathItem.of("d")), TextNode.valueOf("d")); 27 | 28 | final var expected = "The property \"$.a.b.c\" in the expected json is not found\n" + 29 | "The property \"$.a.c\" didn't match. Expected \"a\", Received: \"b\"\n" + 30 | "The property \"$.a.d\" in the received json is not expected\n"; 31 | assertEquals(expected, viewer.toString()); 32 | } 33 | 34 | 35 | @Test 36 | public void canBuildAnOnlyErrorDiffViewerFromJsonDiff() { 37 | final var jsonDiff = new UnMatchedPrimaryDiff(path, TextNode.valueOf("a"), TextNode.valueOf("b")); 38 | 39 | final var result = OnlyErrorDiffViewer.from(jsonDiff); 40 | 41 | final var expected = "The property \"$.a\" didn't match. Expected \"a\", Received: \"b\"\n"; 42 | assertEquals(expected, result.toString()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/viewer/PatchDiffViewerTest.java: -------------------------------------------------------------------------------- 1 | package com.deblock.jsondiff.viewer; 2 | 3 | import com.deblock.jsondiff.DiffGenerator; 4 | import com.deblock.jsondiff.matcher.CompositeJsonMatcher; 5 | import com.deblock.jsondiff.matcher.LenientJsonArrayPartialMatcher; 6 | import com.deblock.jsondiff.matcher.LenientJsonObjectPartialMatcher; 7 | import com.deblock.jsondiff.matcher.LenientNumberPrimitivePartialMatcher; 8 | import com.deblock.jsondiff.matcher.StrictJsonArrayPartialMatcher; 9 | import com.deblock.jsondiff.matcher.StrictJsonObjectPartialMatcher; 10 | import com.deblock.jsondiff.matcher.StrictPrimitivePartialMatcher; 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.io.IOException; 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | 18 | public class PatchDiffViewerTest { 19 | 20 | @Test 21 | public void diffTestUsingLenientDiff() throws IOException { 22 | final var actualJson = Files.readString(Path.of("src/test/java/com/deblock/jsondiff/viewer/actual.json")); 23 | final var expectedJson = Files.readString(Path.of("src/test/java/com/deblock/jsondiff/viewer/expected.json")); 24 | final var expectedDiff = Files.readString(Path.of("src/test/java/com/deblock/jsondiff/viewer/lenientDiff.patch")); 25 | final var jsonMatcher = new CompositeJsonMatcher( 26 | new LenientJsonArrayPartialMatcher(), 27 | new LenientJsonObjectPartialMatcher(), 28 | new LenientNumberPrimitivePartialMatcher(new StrictPrimitivePartialMatcher()) 29 | ); 30 | final var jsondiff = DiffGenerator.diff(expectedJson, actualJson, jsonMatcher); 31 | 32 | final var patchResult= PatchDiffViewer.from(jsondiff); 33 | 34 | Assertions.assertEquals(expectedDiff, patchResult.toString()); 35 | } 36 | 37 | @Test 38 | public void diffTestUsingStrictDiff() throws IOException { 39 | final var actualJson = Files.readString(Path.of("src/test/java/com/deblock/jsondiff/viewer/actual.json")); 40 | final var expectedJson = Files.readString(Path.of("src/test/java/com/deblock/jsondiff/viewer/expected.json")); 41 | final var expectedDiff = Files.readString(Path.of("src/test/java/com/deblock/jsondiff/viewer/strictDiff.patch")); 42 | final var jsonMatcher = new CompositeJsonMatcher( 43 | new StrictJsonArrayPartialMatcher(), 44 | new StrictJsonObjectPartialMatcher(), 45 | new StrictPrimitivePartialMatcher() 46 | ); 47 | final var jsondiff = DiffGenerator.diff(expectedJson, actualJson, jsonMatcher); 48 | 49 | final var patchResult= PatchDiffViewer.from(jsondiff); 50 | 51 | Assertions.assertEquals(expectedDiff, patchResult.toString()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/viewer/actual.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrayWithExtraItem": [1, 2, 3, 6, 7], 3 | "arrayWithNoneMatching": [1, 4, 3], 4 | "arrayWithMissingProperty": [3], 5 | "objectWithMissingPropertyAndNonMatching": { 6 | "a": 1, 7 | "b": 2 8 | }, 9 | "objectWithExtraProperty": { 10 | "a": "1" 11 | }, 12 | "extraObject": { 13 | "a": "1" 14 | }, 15 | "emptyObject": {}, 16 | "arrayWithObjectWithIssue": [ 17 | { 18 | "a": { 19 | "b": [ 20 | { 21 | "c": 1 22 | } 23 | ] 24 | }, 25 | "c": { 26 | "d": 3 27 | } 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/viewer/expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrayWithExtraItem": [1, 2, 3], 3 | "arrayWithNoneMatching": [1, 2, 3], 4 | "arrayWithMissingProperty": [2, 3], 5 | "objectWithMissingPropertyAndNonMatching": { 6 | "b": 5 7 | }, 8 | "objectWithExtraProperty": { 9 | "a": "1", 10 | "c": "5" 11 | }, 12 | "emptyObject": {}, 13 | "arrayWithObjectWithIssue": [ 14 | { 15 | "a": { 16 | "b": [ 17 | { 18 | "c": "1", 19 | "d": 2 20 | } 21 | ] 22 | }, 23 | "c": "3" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/viewer/lenientDiff.patch: -------------------------------------------------------------------------------- 1 | --- actual 2 | +++ expected 3 | @@ @@ 4 | { 5 | "arrayWithObjectWithIssue": [ 6 | { 7 | "a": { 8 | "b": [ 9 | { 10 | - "c": 1 11 | + "c": "1", 12 | + "d": 2 13 | } 14 | ] 15 | }, 16 | - "c": {"d":3} 17 | + "c": "3" 18 | } 19 | ], 20 | "arrayWithExtraItem": [ 21 | 1, 22 | 2, 23 | - 3, 24 | + 3 25 | - 6, 26 | - 7 27 | ], 28 | "arrayWithNoneMatching": [ 29 | 1, 30 | - 4, 31 | + 2, 32 | 3 33 | ], 34 | "objectWithExtraProperty": { 35 | - "a": "1" 36 | + "a": "1", 37 | + "c": "5" 38 | }, 39 | "emptyObject": {}, 40 | "arrayWithMissingProperty": [ 41 | + 2, 42 | 3 43 | ], 44 | "objectWithMissingPropertyAndNonMatching": { 45 | - "b": 2 46 | + "b": 5 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/java/com/deblock/jsondiff/viewer/strictDiff.patch: -------------------------------------------------------------------------------- 1 | --- actual 2 | +++ expected 3 | @@ @@ 4 | { 5 | "arrayWithObjectWithIssue": [ 6 | { 7 | "a": { 8 | "b": [ 9 | { 10 | - "c": 1 11 | + "c": "1", 12 | + "d": 2 13 | } 14 | ] 15 | }, 16 | - "c": {"d":3} 17 | + "c": "3" 18 | } 19 | ], 20 | "arrayWithExtraItem": [ 21 | 1, 22 | 2, 23 | - 3, 24 | + 3 25 | - 6, 26 | - 7 27 | ], 28 | "arrayWithNoneMatching": [ 29 | 1, 30 | - 4, 31 | + 2, 32 | 3 33 | ], 34 | "objectWithExtraProperty": { 35 | - "a": "1" 36 | + "a": "1", 37 | + "c": "5" 38 | }, 39 | "emptyObject": {}, 40 | "arrayWithMissingProperty": [ 41 | - 3 42 | + 2, 43 | + 3 44 | ], 45 | - "extraObject": {"a":"1"}, 46 | "objectWithMissingPropertyAndNonMatching": { 47 | - "a": 1, 48 | - "b": 2 49 | + "b": 5 50 | } 51 | } --------------------------------------------------------------------------------