{"id":25776394,"url":"https://github.com/webcompere/model-assert","last_synced_at":"2026-01-14T02:29:48.088Z","repository":{"id":49580748,"uuid":"372410809","full_name":"webcompere/model-assert","owner":"webcompere","description":"Assertions for data models","archived":false,"fork":false,"pushed_at":"2024-11-17T13:50:47.000Z","size":226,"stargazers_count":31,"open_issues_count":8,"forks_count":3,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-07-11T09:26:57.701Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/webcompere.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"License.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-05-31T06:53:30.000Z","updated_at":"2025-07-05T16:08:55.000Z","dependencies_parsed_at":"2024-11-07T18:18:01.325Z","dependency_job_id":"ef153e20-9dc8-4b07-ace2-ac0d7be67878","html_url":"https://github.com/webcompere/model-assert","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/webcompere/model-assert","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webcompere%2Fmodel-assert","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webcompere%2Fmodel-assert/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webcompere%2Fmodel-assert/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webcompere%2Fmodel-assert/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/webcompere","download_url":"https://codeload.github.com/webcompere/model-assert/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webcompere%2Fmodel-assert/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408711,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-02-27T06:01:22.990Z","updated_at":"2026-01-14T02:29:48.082Z","avatar_url":"https://github.com/webcompere.png","language":"Java","funding_links":[],"categories":["测试"],"sub_categories":[],"readme":"# ModelAssert\n\n[![Build on Push](https://github.com/webcompere/model-assert/actions/workflows/build-actions.yml/badge.svg)](https://github.com/webcompere/model-assert/actions/workflows/build-actions.yml)[![codecov](https://codecov.io/gh/webcompere/model-assert/branch/main/graph/badge.svg?token=SJ9ZKQVO5T)](https://codecov.io/gh/webcompere/model-assert)\n\n\nAssertions for model data. Inspired by [JSONAssert](https://github.com/skyscreamer/JSONassert)\nand [AssertJ](https://assertj.github.io/doc/). Built on top of [Jackson](https://github.com/FasterXML/jackson).\n\nIntended as a richer way of writing assertions in unit tests, and as\na more powerful alternative to Spring's [`jsonPath`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/result/MockMvcResultMatchers.html#jsonPath-java.lang.String-org.hamcrest.Matcher-).\n\nDescribes paths using [JSON Pointer](https://gregsdennis.github.io/Manatee.Json/usage/pointer.html) syntax, where\na route to the element is a series of `/` delimited field names or array indices.\n\n## Installation\n\nModelAssert requires Java 8.\n\nInstall from Maven Central:\n\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003euk.org.webcompere\u003c/groupId\u003e\n  \u003cartifactId\u003emodel-assert\u003c/artifactId\u003e\n  \u003cversion\u003e1.1.0\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n## Quickstart\n\nFor a walk-through of key features, there's a [tutorial over on Baeldung.com](https://www.baeldung.com/json-modelassert).\n\n### Path Assertions\n\n```java\nString json = \"{\\\"name\\\":\\\"ModelAssert\\\"}\";\n\n// assertJ style\nassertJson(json)\n   .at(\"/name\").hasValue(\"ModelAssert\");\n\n// hamcrest style\nMatcherAssert.assertThat(json,\n    json()\n      .at(\"/name\").hasValue(\"ModelAssert\"));\n```\n\nIn the above example, `at` is just one of the possible conditions. Here we see Jackson's JSON Pointer syntax in action too.\n\n### Whole JSON Comparison\n\nSemantic comparison of the JSON loaded as both expected and actual.\n\n```java\n// assertJ style\nassertJson(\"{\\\"name\\\":\\\"ModelAssert\\\",\\\"versions\\\":[1.00, 1.01, 1.02]}\")\n    .isEqualTo(\"{\\\"name\\\":\\\"ModelAssert\\\",\\\"versions\\\":[1.00, 1.01, 1.02]}\");\n\n// hamcrest style\nMatcherAssert.assertThat(\"{\\\"name\\\":\\\"ModelAssert\\\",\\\"versions\\\":[1.00, 1.01, 1.02]}\",\n    json().isEqualTo(\"{\\\"name\\\":\\\"ModelAssert\\\",\\\"versions\\\":[1.00, 1.01, 1.02]}\"));\n```\n\nThese comparisons can be mixed with path asserts, but they compare the whole\nobject structure and report the differences on error, so there's minimum benefit in using both.\n\nBy default, the comparison must match everything in order, but the `isEqualTo`\ncan be relaxed by using `where`:\n\n```java\n// allow object keys in any order\nassertJson(\"{\\\"name\\\":\\\"ModelAssert\\\",\\\"versions\\\":[1.00, 1.01, 1.02]}\")\n    .where()\n        .keysInAnyOrder()\n    .isEqualTo(\"{\\\"versions\\\":[1.00, 1.01, 1.02], \\\"name\\\":\\\"ModelAssert\\\"}\");\n```\n\nSee [where context](#where-context) for more examples.\n\n### Assertion DSL\n\nThere are more examples in the unit tests, especially [`ExamplesTest`](src/test/java/uk/org/webcompere/modelassert/json/ExamplesTest.java).\n\nThe `assertJson` methods produce stand-alone assertions which\nexecute each clause in order, stopping on error.\n\nThe `json*` methods - `json`, `jsonNode`, `jsonFile`, `jsonFilePath` start the\nconstruction of a hamcrest matcher to which conditions are added.\nThese are evaluated when the hamcrest matcher's `matches` is called.\n\n\u003e Note: the DSL is intended to provide auto-complete and is largely fluent.\n\u003e It's also composable, so multiple comparisons can be added after the\n\u003e last one is complete:\n\n```java\nassertJson(json)\n   .at(\"/name\").hasValue(\"ModelAssert\")\n   .at(\"/license\").hasValue(\"MIT\")\n   .at(\"/price\").isNull();\n```\n\n### Non JSON Comparison\n\nIf an object can be converted into Jackson's `JsonNode` structure, which nearly everything can be,\nthen it can be compared using ModelAssert:\n\n```java\nMap\u003cString, Object\u003e objectMap = new HashMap\u003c\u003e();\nobjectMap.put(\"a\", UUID.randomUUID().toString());\nobjectMap.put(\"b\", UUID.randomUUID().toString());\n\nMap\u003cString, Object\u003e expectedMap = new HashMap\u003c\u003e();\nexpectedMap.put(\"a\", \"\");\nexpectedMap.put(\"b\", \"\");\n\nassertJson(objectMap)\n    .where()\n    .path(Pattern.compile(\"[ab]\")).matches(GUID_PATTERN)\n    .isEqualTo(expectedMap);\n```\n\nAs both `assertJson` and `isEqualTo` allow `JsonNode` as an input,\ncustom conversions to this can be used from any source.\n\n### YAML Support\n\nAs Jackson can load yaml files, the DSL also supports `assertYaml` and `isEqualToYaml`/`isNotEqualToYaml`:\n\n```java\nString yaml1 =\n    \"name: Mr Yaml\\n\" +\n        \"age: 42\\n\" +\n        \"items:\\n\" +\n        \"  - a\\n\" +\n        \"  - b\\n\";\n\nString yaml2 =\n    \"name: Mrs Yaml\\n\" +\n        \"age: 43\\n\" +\n        \"items:\\n\" +\n        \"  - c\\n\" +\n        \"  - d\\n\";\n\nassertYaml(yaml1)\n    .isNotEqualToYaml(yaml2);\n```\n\nThe Hamcrest version of this uses `yaml`/`yamlFile` and `yamlFilePath`:\n\n```java\nMatcherAssert.assertThat(yaml1, yaml().isEqualToYaml(yaml2));\n```\n\n### Manipulating Json Before or During Assertions\n\nThe assertion DSL allows a lot of navigation within the json under test.\nHowever, it may be desirable to manually load some json for comparison, and perhaps\nuse only a part of that json:\n\n```java\n// load some json to compare against\nJsonNode jsonNode = JsonProviders.jsonPathProvider().jsonFrom(jsonFile);\n\n// compare \"/child\" within a source\nassertJson(jsonFile)\n    .at(\"/child\")\n\n    // must be equal to the \"/child\" we've selected\n    // from an \"actual\"\n    .isEqualTo(jsonNode.at(\"/child\"));\n```\n\n## Building the Assertion\n\nThe entry point to creating an assertion is:\n\n- `assertJson` - overloaded to take JSON as `String`, `JsonNode`, `File` or `Path` - **produces a fluent assertion like AssertJ**\n  \u003e Note: the Jackson parser has been configured to load unquoted field names\n  \u003e so:\n  \u003e ```java\n  \u003e String unquoted = \"{someField: \\\"value\\\"}\";\n  \u003e // is equivalent to\n  \u003e String quoted = \"{\\\"someField\\\": \\\"value\\\"}\";\n  \u003e ```\n  \u003e Examples throughout the tests are in the second, more conventional, format.\n- `json` - start creating a hamcrest matcher for a `String`\n- `jsonNode` - start creating a hamcrest matcher for a `JsonNode`\n- `jsonFile` - start creating a hamcrest matcher for a `File`\n- `jsonFilePath` - start creating a hamcrest matcher for a `Path`\n\nAfter that, there are high level methods to add conditions to the matcher:\n\n- `at` - start creating a JSON Pointer based assertion\n- `isNull`/`isNotNull` - asserts whether the whole loaded JSON amounts to `null`\n- `isEqualTo`/`isNotEqualTo` - compare this tree against another\n- `satisfies` - plug in a custom `Condition` or `ConditionList`\n\nWhen a condition has been added to the assertion then the fluent DSL\nallows for further conditions to be added.\n\n\u003e Note: the `assertJson` version executes each condition on the fly, where the hamcrest\nversion stores them for execution until the `matches` method is invoked by `MatcherAssert.assertThat`\nor similar.\n\n## Conditions\n\nThere are multiple contexts from which assertions are available:\n\n- **Assertion** - this allows `at` as well as ALL other assertions\n- **Inside `at`** - allows any `node` assertion, and then returns to `assertion` context\n- **Node** - this allows any assertion on the current node, which may be of any valid json type as well as `missing`\n- **Type specific** - by calling `number`, `text`, `object`, `array`, or `booleanNode` on a node context DSL, the DSL\ncan be narrowed down to assertions for just that type - this can also be more expressive\n    ```java\n    assertJson(json)\n       .at(\"/name\").text().isText(\"My Name\");\n    ```\n    it's probably more _correct_ to specify the type and then use assertions relating to that type, but as\n    a shorthand, the assertions are all available without an additional type specifier. Using the type,\n    implicitly creates a check that the node is the correct type, so:\n    ```java\n    assertJson(json)\n       .at(\"/name\").text().isEmpty();\n    ```\n    is the equivalent of:\n    ```java\n    assertJson(json)\n       .at(\"/name\").isText()\n       .at(\"/name\").isEmpty();\n    ```\n- **Where** - called before `isEqualTo` to create rules for whole tree comparison\n\n### Json At\n\nBuild a `JsonAt` condition by using `.at(\"/some/json/pointer\")`.\n\nThis is then followed by any of the node context assertions.\n\nExample:\n\n```java\nassertJson(\"{\\\"name\\\":null}\")\n    .at(\"/name\").isNull();\n```\n\nThe `JsonAt` expression is incomplete with just `at`, but once the rest of the condition is added,\nthe `this` returned belongs to the main assertion, allowing them to be chained.\n\n```java\nassertJson(\"{\\\"name\\\":null}\")\n    .at(\"/name\").isNull()\n    .at(\"/address\").isMissing();\n```\n\nJSON Pointer expressions treat field names and array indices as `/` delimited:\n\n```java\nassertJson(\"{\\\"names\\\":[\\\"Model\\\",\\\"Assert\\\"]}\")\n    .at(\"/names/1\").hasValue(\"Assert\");\n```\n\n### Node Context Assertions\n\nThese are available on any node of the tree, which might be any type. They include the\ntype specific assertions below, as well as:\n\n- `hasValue` - assert that a field has a specific value\n  ```java\n  assertJson(jsonString)\n    .at(\"/name\").hasValue(\"ModelAssert\");\n  ```\n  \u003e Note: this is very forgiving of type, and may be less precise as\n  \u003e a consequence. It detects the expected node type from its input.\n- `isNull`/`isNotNull` - assert whether this path resolves to `null`\n  ```java\n  assertJson(jsonString)\n    .at(\"/price\").isNull();\n  ```\n- `isMissing`/`isNotMissing` - assert that this path resolves to _missing_ - i.e. it's an unknown path in the JSON\n  ```java\n  assertJson(jsonString)\n    .at(\"/random\").isMissing();\n  ```\n- `isAnyNode` - the same as `isNotMissing` - useful when used with `.where()` in full tree matching\n- `isEmpty`/`isNotEmpty` - assert that the json at this location\n  is an empty text, array, or object node\n  ```java\n  assertJson(someJson)\n    .isEmpty();\n  ```\n  This can be combined with a more precise type check and a path in the json:\n  ```java\n  assertJson(someJson)\n    .at(\"/name\").isText()\n    .at(\"/name\").isEmpty();\n  ```\n  or, better still:\n  ```java\n  assertJson(someJson)\n    .at(\"/name\").text().isEmpty();\n  ```\n  Though for brevity, the `isEmptyText`/`isNotEmptyText` may be easier:\n    ```java\n  assertJson(someJson)\n    .at(\"/name\").isEmptyText();\n  ```\n  \u003e [!WARNING]\n  \u003e Unless you're 100% sure of the type of a node, `isEmpty` and `isNotEmpty` are too approximate and\n  \u003e should be combined with a DSL-switching type assertion like `.text()`, `.object()`, or `.array()`\n  \u003e since `isEmpty` on a `Boolean` makes no sense, and it's not clear what `isNotEmpty` or `isEmpty` would\n  \u003e mean on `null`.\n- `matches(Matcher\u003cJsonNode\u003e)` - assert that the **node** found at this JSON path matches a hamcrest matcher for `JsonNode`\n  ```java\n  assertJson(jsonString)\n    .at(\"/child/someobject\").matches(customHamcrestMatcher);\n  ```\n  This latter example, allows us to reuse the hamcrest form of the\n  json assertion across tests, if there's a common pattern, or allows\n  us to apply a particular set of assertions to only a subtree of the original:\n  ```java\n  assertJson(jsonString)\n    .at(\"/root/child/otherchild/interestingplace\")\n    .matches(jsonNode()  // jsonNode() creates a new matcher\n       .at(\"/name\").hasValue(\"Model\")\n       .at(\"/age\").hasValue(42));\n  ```\n  \u003e Note: `satifies` along with `ConditionList` may be a better solution to subtree\n  \u003e assertions with `at`\n  \u003e ```java\n  \u003e assertJson(\"[\" +\n  \u003e   \"{\\\"name\\\":\\\"Model\\\",\\\"ok\\\":true},\" +\n  \u003e   \"{\\\"name\\\":\\\"Model\\\",\\\"ok\\\":false},\" +\n  \u003e   \"{\\\"name\\\":\\\"Model\\\"},\" +\n  \u003e   \"{\\\"age\\\":1234}\" +\n  \u003e   \"]\")\n  \u003e   .at(\"/1\").satisfies(conditions()\n  \u003e     .at(\"/name\").hasValue(\"Model\")\n  \u003e     .at(\"/ok\").isFalse());\n  \u003e```\n- `is`/`isNot` - provide a description and a `Predicate\u003cJsonNode\u003e` to customise with a custom match condition\n  \u003e This is the unlimited customisable assertion - allowing any test to be done on a per node basis, if it's\n  \u003e not already part of the DSL\n  ```java\n  assertJson(\"42\")\n    .is(\"Even number\", jsonNode -\u003e jsonNode.isNumber() \u0026\u0026 jsonNode.asInt() % 2 == 0);\n  ```\n- `is(Function)` - allows customisation with a standard set of match conditions - to modularise the tests:\n  ```java\n  @Test\n  void canApplyStandardSetOfAssertions() {\n      assertJson(\"{\\\"root\\\":{\\\"name\\\":\\\"Mr Name\\\"}}\")\n        .is(ExamplesTest::theUsual)\n        .isNotEmpty(); // additional clause\n  }\n  private static \u003cA\u003e A theUsual(JsonNodeAssertDsl\u003cA\u003e assertion) {\n      return assertion.at(\"/root/name\").isText(\"Mr Name\");\n  }\n  ```\n\n### Text Context Conditions\n\n- `isText`/`isNotText` - assert that the node is a text node, with optional specific text - note: this can also be achieved with `hasValue`, but adds\nsome extra checking that this is a text node\n  ```java\n  assertJson(\"\\\"theText\\\"\")\n    .isText();\n\n  assertJson(\"\\\"theText\\\"\")\n    .isText(\"theText\");\n\n  assertJson(\"{\\\"child\\\":{\\\"age\\\":123}}\")\n    .at(\"/child/age\").isNotText();\n\n    assertJson(\"{\\\"child\\\":{\\\"name\\\":\"Bob\"}}\")\n    .at(\"/child/age\").isNotText(\"Bert\");\n  ```\n- `isEmptyText`/`isNotEmptyText` - both of these require the node to be text, and then assert that the text is `\"\"` or not\n  ```java\n  assertJson(\"\\\"\\\"\")\n    .isEmptyText();\n\n  // FAILS! - wrong type\n  assertJson(\"0\")\n    .isNotEmptyText();\n\n  // non empty\n  assertJson(\"\\\"0\\\"\")\n    .isNotEmptyText();\n  ```\n- `matches(Pattern|String)` - assert that the **text** of this node matches a regular expression - some common patterns are available in the `Patterns` class\n  ```java\n  assertJson(jsonString)\n    .at(\"/guid\").matches(GUID_PATTERN);\n  ```\n- `textMatches`- allows a custom predicate to be passed in order to perform a custom check\n  ```java\n  assertJson(\"\\\"a-b-c\\\"\")\n    .textMatches(\"Has dashes\", text -\u003e text.contains(\"-\"));\n  ```\n- `textContains`/`textDoesNotContain` - reuses the logic of the regular expression matcher to find substrings\n- `textStartsWith`/`textDoesNotStartWith` - reuses the logic of the regular expression matcher to check the prefix of a text node's text\n\n### Numeric Context Conditions\n- `isGreaterThan`, `isGreaterThanOrEqualTo`, `isLessThan`, `isLessThanOrEqualTo` - these\n  require that the node is a number of a numeric type, and compares\n  ```java\n  assertJson(jsonString)\n     .at(\"/count\").isGreaterThan(9);\n  ```\n  More specific typed versions - `isGreaterThanInt` or `isLessThanLong` also exist to avoid a test\n  passing through accidental type coercion or overflow.\n- `isBetween` - asserts that a number falls in a range\n  ```java\n  assertJson(\"{number:12}\")\n    .at(\"/number\").isBetween(2, 29);\n  ```\n- `isZero` - asserts that the number is zero\n- `isNumber`, `isInteger`, `isLong`, `isDouble` - assert this is a numeric node\nor of a specific numeric type\n\n### Boolean Context Conditions\n- `isTrue`/`isFalse` - requires the node to be boolean and have the correct value\n- `isBoolean`/`isNotBoolean` - asserts the type of the node\n\n### Object Context Conditions\n- `isObject`/`isNotObject` - asserts the type of the node\n- `containsKey`/`containsKeys`/`doesNotContainKey`/`doesNotContainKeys` - checks for the presence of a given set of keys in the object\n- `containsKeysExactly` - requires the given keys to be present in the exact order provided\n- `containsKeysExactlyInAnyOrder` - requires the given keys all to be present, regardless of order in the JSON\n\n### Array Context Conditions\n- `isArray`/`isNotArray` - asserts the type of the node\n- `isArrayContaining`/`isArrayContainingExactlyInAnyOrder` - **potentially slow** assertions over the contents\n   of an array. Tries all permutations of matching the provided elements to the array elements, allowing for\n   duplicates. Uses loose `hasValue` style matching when values provided:\n   ```java\n   assertJson(\"[1, 2, 3, 4]\")\n      .isArrayContaining(1, 4);\n\n   assertJson(\"[1, 2, 3, 4]\")\n      .isArrayContainingExactlyInAnyOrder(1, 2, 3, 4);\n   ```\n- `isArrayContainingExactly` - strictly proves that each element in the array\n  matches the elements provided:\n  ```java\n    assertJson(\"[1, 2, 3, 4]\")\n      .isArrayContainingExactly(1, 2, 3, 4);\n  ```\n  This is more efficient at runtime as it has a simple job.\n\nThere are two main ways to assert the contents of an array. It can be done by\nvalue as illustrated above, or it can be done by condition list.\n\nTo use the `isArrayContaining` suite of functions with a condition list,\nwe call `conditions()` within the `ConditionList` class to create a\nfluent builder of a list of conditions. As the fluent builder for assertions\nadds conditions to the assertion, so the fluent builder inside\n`ConditionList` treats each additional condition as an element to search for\nin the array:\n\n```java\nassertJson(\"[\" +\n    \"{\\\"name\\\":\\\"Model\\\",\\\"ok\\\":true},\" +\n    \"{\\\"name\\\":\\\"Model\\\",\\\"ok\\\":false},\" +\n    \"{\\\"name\\\":\\\"Assert\\\"},\" +\n    \"{\\\"age\\\":1234}\" +\n    \"]\")\n    .isArrayContainingExactlyInAnyOrder(conditions()\n        .at(\"/name\").isText(\"Assert\")\n        .at(\"/name\").hasValue(\"Model\")\n        .at(\"/ok\").isFalse()\n        .at(\"/age\").isNumberEqualTo(1234));\n```\n\nIn the above example, the conditions, between them, represent a unique\nmatch in each element of the list, but a condition may match more than one\nelement (as `.at(\"/name\".isText(\"Assert\")` does). This is where the\npermutational search of the ArrayCondition helps to find the best possible match.\n\nWhere a single condition cannot describe the required match for an element\nthen `satisfies`, which is part of every node, allows a `ConditionList`:\n\n```java\nassertJson(\"[\" +\n    \"{\\\"name\\\":\\\"Model\\\",\\\"ok\\\":true},\" +\n    \"{\\\"name\\\":\\\"Model\\\",\\\"ok\\\":false},\" +\n    \"{\\\"name\\\":\\\"Model\\\"},\" +\n    \"{\\\"age\\\":1234}\" +\n    \"]\")\n    .isArrayContainingExactlyInAnyOrder(conditions()\n        // condition A\n        .at(\"/name\").isText(\"Model\")\n\n        // condition B\n        .satisfies(conditions()\n            .at(\"/name\").hasValue(\"Model\")\n            .at(\"/ok\").isTrue())\n\n        // condition C\n        .satisfies(conditions()\n            .at(\"/ok\").isFalse()\n            .at(\"/name\").isText(\"Model\"))\n\n        // condition D\n        .at(\"/age\").isNumberEqualTo(1234));\n```\n\nEach of these composite conditions allows the whole DSL. They're\ncomposed together using `Condition.and`.\n\n\u003e A Hamcrest matcher could also be used with `ConditionList`\n\u003e via `matches(Matcher\u003cJsonNode\u003e)`\n\n## Size Assertions (various types)\n\nObject, String and Array can be said to be _sizeable_. For Object, the size is\nthe number of keys. For String, it's the number of characters. For Array it's the number\nof elements.\n\nWe can assert this with `hasSize`:\n\n```java\nassertJson(\"\\\"some string\\\"\")\n    .hasSize(11);\n\nassertJson(\"[1, 2, 3]\")\n    .hasSize(3);\n```\n\nThe general purpose `Number` based numeric assertions can be used to assert size via the\n`size()` function, which enters the `NumberComparison` context:\n\n```java\n// assert that the array has a size between 3 and 9\nassertJson(\"[1, 2, 3]\")\n    .size().isBetween(3, 9);\n```\n\n## Whole Tree Comparison\n\nThe tree comparison is intended to perform a semantic comparison of a JSON\ntree with another.\n\nIt can be used in conjunction with the `at` part of the Node DSL:\n\n```java\nassertJson(\"{\\\"name\\\":\\\"ModelAssert\\\",\\\"versions\\\":[1.00, 1.01, 1.02]}\")\n    .at(\"/versions\")\n    .isEqualTo(\"[1.00, 1.01, 1.02]\");\n```\n\nIt can also be customised using `where`.\n\n### Where Context\n\nThis is used to customise how whole tree comparison works.\n\nThe `where` function moves us from node context to customisation of `isEqualTo`:\n\n```java\nassertJson(\"{\\\"name\\\":\\\"ModelAssert\\\",\\\"versions\\\":[1.00, 1.01, 1.02]}\")\n    .where()\n        .keysInAnyOrder()\n    .isEqualTo(\"{\\\"versions\\\":[1.00, 1.01, 1.02], \\\"name\\\":\\\"ModelAssert\\\"}\");\n```\n\nIn the where context, we can add general leniency overrides, or specify overrides\nfor particular paths.\n\n- `keysInAnyOrder`/`keysInOrder` - controls whether objects observe order checks - when used just after `where` this applies to the whole tree, otherwise it applies to the path exression\n- `objectContains` - the object ignored missing values in the _actual_\n- `arrayInAnyOrder` - array elements can be in any order\n- `arrayContains` - array elements can be in any order and the actual may have additional elements\n- `path` - start customising the rule for a particular path in the tree:\n  ```java\n  // turn off key order sensitivity for the `address` field\n  assertJson(...)\n     .where().path(\"address\").keysInAnyOrder()\n     .isEqualTo(...);\n  ```\n  The path is expressed as a series of values, which can be:\n  - `String` - conforming to a JSON Pointer, but no `/`\n  - Regular expression for matching a field - i.e. `Pattern`\n  - `PathWildCard` - either `ANY` or `ANY_SUBTREE` - allowing path matching of one or n levels of fields\n- `at` - a synonym for `path` where the whole JSON Pointer style path is provided - this is a short-hand for paths where there are no wildcards\n\nWithin the path expression, we then add further conditions:\n\n- Any conditions from Node context\n- `keysInAnyOrder`/`keysInOrder` - specific matches for the current path\n- `objectContains` - the object ignored missing values in the _actual_\n- `arrayInAnyOrder` - array elements can be in any order\n- `arrayContains` - array elements can be in any order and the actual may have additional elements\n- `isIgnored` - the path is just ignored\n\nThe purpose of the `where` and `path` contexts is to allow for things\nwhich cannot be predicted at the time of coding, or which do not matter\nto the result.\n\nA good example is GUIDs in the output. Let's say we have a process which\nproduces JSON with random GUIDs in it. We want to assert that there ARE GUIDs\nbut we can't predict them:\n\n```java\nassertJson(\"{\\\"a\\\":{\\\"guid\\\":\\\"fa82142d-13d2-49c4-9878-619c90a9f986\\\"},\" +\n    \"\\\"b\\\":{\\\"guid\\\":\\\"96734f31-33c3-4e50-a72b-49bf2d990e33\\\"},\" +\n    \"\\\"c\\\":{\\\"guid\\\":\\\"064c8c5a-c9c1-4ea0-bf36-1994104aa870\\\"}}\")\n    .where()\n        .path(ANY_SUBTREE, \"guid\").matches(GUID_PATTERN)\n    .isEqualTo(\"{\\\"a\\\":{\\\"guid\\\":\\\"?\\\"},\" +\n        \"\\\"b\\\":{\\\"guid\\\":\\\"?\\\"},\" +\n        \"\\\"c\\\":{\\\"guid\\\":\\\"?\\\"}}\");\n```\n\nHere, the `path(ANY_SUBTREE, \"guid\").matches(GUID_PATTERN)` phrase is\nallowing anything _ending_ in `guid` to be matched using `matches(GUID_PATTERN)`\ninstead of matching it against the JSON inside `isEqualTo`.\n\nThis can be done more specifically using `at`:\n\n```java\nassertJson(\"{\\\"a\\\":{\\\"guid\\\":\\\"fa82142d-13d2-49c4-9878-619c90a9f986\\\"},\" +\n    \"\\\"b\\\":{\\\"guid\\\":\\\"96734f31-33c3-4e50-a72b-49bf2d990e33\\\"},\" +\n    \"\\\"c\\\":{\\\"guid\\\":\\\"064c8c5a-c9c1-4ea0-bf36-1994104aa870\\\"}}\")\n    .where()\n        .at(\"/a/guid\").matches(GUID_PATTERN)\n        .at(\"/b/guid\").matches(GUID_PATTERN)\n        .at(\"/c/guid\").matches(GUID_PATTERN)\n    .isEqualTo(\"{\\\"a\\\":{\\\"guid\\\":\\\"?\\\"},\" +\n        \"\\\"b\\\":{\\\"guid\\\":\\\"?\\\"},\" +\n        \"\\\"c\\\":{\\\"guid\\\":\\\"?\\\"}}\");\n```\n\n\u003e Note: the rules used with `where` are evaluated in reverse order\n\u003e so the most general should be provided first, and the most specific last.\n\n#### Loose Array Matching\n\n**Warning: performance implications** both `arrayInAnyOrder` and `arrayContains`\ntry every possible combination of array element in the expected against the\nactual in order to work out if the expected elements are present. For\nsmall arrays, this is not a problem, and the unit tests of this project\nrun very quickly, proving that.\n\nHowever, an array can, itself, contain objects or other arrays. This can lead\nto a large permutational explosion, which can take time.\n\nThe easiest way to relax array ordering rules is to use `where().arrayInAnyOrder()`\nwhile setting up `isEqualTo`:\n\n```java\nassertJson(\"{\\\"name\\\":\\\"ModelAssert\\\",\\\"versions\\\":[1.00, 1.01, 1.02]}\")\n    .where()\n    .arrayInAnyOrder()\n    .isEqualTo(\"{\\\"name\\\":\\\"ModelAssert\\\", \\\"versions\\\":[1.02, 1.01, 1.00]}\");\n```\n\nIf only a specific array may be in a random order, it may be better to specialise\nthis by path:\n\n```java\nassertJson(\"{\\\"name\\\":\\\"ModelAssert\\\",\\\"versions\\\":[1.00, 1.01, 1.02]}\")\n    .where()\n    .path(\"versions\").arrayInAnyOrder()\n    .isEqualTo(\"{\\\"name\\\":\\\"ModelAssert\\\", \\\"versions\\\":[1.02, 1.01, 1.00]}\");\n```\n\nAnd, if the value in the expected doesn't contain all the values from the\narray in the actual, then we can use `arrayContains` to both relax the order\nand allow matching of the ones found:\n\n```java\nassertJson(\"{\\\"name\\\":\\\"ModelAssert\\\",\\\"versions\\\":[1.00, 1.01, 1.02]}\")\n    .where()\n    .path(\"versions\").arrayContains()\n    .isEqualTo(\"{\\\"name\\\":\\\"ModelAssert\\\", \\\"versions\\\":[1.02]}\");\n```\n\n\u003e Note: loose array comparison also honours the rules set in where\n\u003e for the child nodes of the array. **The paths described are routes within\n\u003e the actual tree, not the expected tree.**. So as every combination of\n\u003e match is tried, the path rules may perform different comparisons on the\n\u003e expected data, as it's checked against each actual.\n\n#### Common `where` Configuration\n\nThe `configuredBy` function on the `WhereDsl` allows a common comparison configuration\nto be implemented and plugged in:\n\n```java\n@Test\nvoid matchesAnyGuidUsingCommonConfiguration() {\n    assertJson(\"{\\\"a\\\":{\\\"guid\\\":\\\"fa82142d-13d2-49c4-9878-619c90a9f986\\\"},\" +\n        \"\\\"b\\\":{\\\"guid\\\":\\\"96734f31-33c3-4e50-a72b-49bf2d990e33\\\"},\" +\n        \"\\\"c\\\":{\\\"guid\\\":\\\"064c8c5a-c9c1-4ea0-bf36-1994104aa870\\\"}}\")\n        .where()\n            .configuredBy(ExamplesTest::ignoreGuids)\n        .isEqualTo(\"{\\\"a\\\":{\\\"guid\\\":\\\"?\\\"},\" +\n            \"\\\"b\\\":{\\\"guid\\\":\\\"?\\\"},\" +\n            \"\\\"c\\\":{\\\"guid\\\":\\\"?\\\"}}\");\n}\n\nprivate static \u003cA\u003e WhereDsl\u003cA\u003e ignoreGuids(WhereDsl\u003cA\u003e where) {\n    return where.path(ANY_SUBTREE, \"guid\").matches(GUID_PATTERN);\n}\n```\n\n## Customisation\n\nThere's room for custom assertions throughout the DSL, and if necessary,\nthe `Satisfies` interface, allows a condition to be added fluently. Conditions\nare based on the `Condition` class. The existing conditions can be used directly\nif necessary, and can be composed using `Condition.and` or `Condition.or`\nwhere needed. Similarly, there's a `not` method in the `Condition`\nclass `Not` to invert any condition as well as `invert` on `Condition` to invert\nthe current condition.\n\nA custom condition can be fed to `satisfies`:\n\n```java\n// using `and` along with functions from the\n// condition classes\nassertJson(\"\\\"some string\\\"\").satisfies(\n    textMatches(Pattern.compile(\"[a-z ]+\"))\n        .and(new HasSize(12)));\n\n// using or and inverting the condition - this will\n// pass as it fails both the ORed conditions, but the\n// whole statement is inverted\nassertJson(\"\\\"some string!!!\\\"\").satisfies(\n    textMatches(Pattern.compile(\"[a-z ]+\"))\n        .or(new HasSize(12))\n        .inverted());\n```\n\n## Interoperability\n\nThe assertions can be used stand-alone with `assertJson` or can be built as Hamcrest matchers. The assertion\ncan also be converted to a `Mockito` `ArgumentMatcher`.\n\n### Mockito Usage\n\nAssuming Mockito 3, the `toArgumentMatcher` method converts the `Hamcrest` style syntax into Mockito's native\n`ArgumentMatcher`. Older versions of `Mockito` used Hamcrest natively.\n\nThe json matcher can then be used to detect calls to a function either with `verify`/`then` or when setting\nup responses to different inputs:\n\n```java\n// detecting calls based on the json values passed\nsomeInterface.findValueFromJson(\"{\\\"name\\\":\\\"foo\\\"}\");\n\nthen(someInterface)\n        .should()\n        .findValueFromJson(argThat(json()\n        .at(\"/name\").hasValue(\"foo\")\n        .toArgumentMatcher()));\n\n\n// setting up responses based on the json\ngiven(someInterface.findValueFromJson(argThat(json()\n        .at(\"/name\").hasValue(\"foo\")\n        .toArgumentMatcher())))\n        .willReturn(\"foo\");\n\nassertThat(someInterface.findValueFromJson(\"{\\\"name\\\":\\\"foo\\\"}\")).isEqualTo(\"foo\");\n```\n\nNote, this works with all the types of JSON input sources supported by the Hamcrest version of the library.\nYou need to choose the type of input via the `json`, `jsonFile` methods etc.\n\n### Interoperability with Spring MVC Matchers\n\nRather than:\n\n```java\n// clause inside ResultMatcher\njsonPath(\"$.name\", \"ModelAssert\")\n```\n\nWe can construct the hamcrest matcher version of ModelAssert's JsonAssertion:\n\n```java\ncontent().string(\n    json()\n        .at(\"/name\")\n        .hasValue(\"ModelAssert\"))\n```\n\nWhile this syntax is of limited value in this simple case, the more powerful comparisons supported\nby this library are equally possible after the `json()` statement starts creating a matcher.\n\n### Custom Object Mappers\n\nBy default, Model Assert uses two `ObjectMapper` objects - one for loading JSON and one for loading YAML.\nThese can be overridden for the current thread (allowing concurrent testing) and it's advisable to do this\nin the `@BeforeAll` of a text fixture:\n\n```java\n@BeforeAll\nstatic void beforeAll() {\n    // support LocalDateTime\n    overrideObjectMapper(defaultObjectMapper()\n        .registerModule(new JavaTimeModule())\n        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS));\n\n    // stop parsing `yes` to mean Boolean `true`\n    overrideYamlObjectMapper(new ObjectMapper(\n        new YAMLFactory()\n            .configure(PARSE_BOOLEAN_LIKE_WORDS_AS_STRINGS, true)));\n}\n```\n\nand when replacing the object mapper in setup, it's a good idea to put it back in the tear down:\n\n```java\n@AfterAll\nstatic void afterAll() {\n    clearObjectMapperOverride();\n    clearYamlObjectMapperOverride();\n}\n```\n\nAny assertions used while the override is in place will use the alternative object mapper.\n\n\u003e Note: if using a common alternative object mapper, maybe consider building a small JUnit 5 test extension\n\u003e or [use a base class](./src/test/java/uk/org/webcompere/modelassert/json/OverrideObjectMapper.java) for your tests\n\u003e which contains the common set up\n\nThe functions `defaultObjectMapper` and `defaultYamlMapper` in `JsonProviders` can be used to create a basic `ObjectMapper`\nto base a custom one on.\n\n## API Stability\n\nThe classes in the root package `uk.org.webcompere.modelassert.json` are the jumping\non point for the API and they will be changed rarely.\n\nFunctions elsewhere will be accessed via the fluent API and may move between packages\nin later versions, though this should be resolved without changing consuming code.\n\nSemVer numbering will indicate possible breaking changes by increments to the minor version number. Patch\nversions are unlikely to have any noticeable effect on the API.\n\n## Contributing\n\nIf you experience any problems using this library, or have any ideas, then please\n[raise an issue](https://github.com/webcompere/model-assert/issues/new/choose). Please\ncheck for any [existing issues](https://github.com/webcompere/model-assert/issues) first.\n\nPRs will be accepted if they come with unit tests and are linked to an issue.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebcompere%2Fmodel-assert","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwebcompere%2Fmodel-assert","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebcompere%2Fmodel-assert/lists"}