In REST Assured, why does then().body(path, equalTo(0.82)) fail on a JSON decimal?
answer
- the box type, not the arithmetic
- decimals narrow, integers do not
- equalTo calls equals
- one letter fixes it
- or change the number return type
basics
~20 sREST Assured parses a JSON decimal into a Float by default. Hamcrest's equalTo compares with equals, so a Java double literal like 0.82 never matches the parsed Float. Write 0.82f instead, or change the configured number return type.
solid answer
~50 sBy default REST Assured materialises a non-integer JSON number as a `java.lang.Float`. The underlying Groovy slurper produces a `BigDecimal`, and REST Assured's own slurper converts it under the default `NumberReturnType.FLOAT_AND_DOUBLE` — to a `Float` unless the value exceeds `Float.MAX_VALUE`, in which case it becomes a `Double`. `equalTo(0.82)` builds a matcher over a boxed `Double`, and `Float.equals(Double)` is false regardless of the arithmetic, so the assertion fails with a message that looks like a rounding problem. The fixes, in order of preference: write the float literal, `equalTo(0.82f)`; or read the value through a coercing getter such as `jsonPath().getDouble("glossary.entry[0].confidence")`; or switch the whole suite to `NumberReturnType.BIG_DECIMAL`. Whole numbers are unaffected — they arrive as `Integer` or `Long`, so `equalTo(3)` matches `reviewCount` normally. The failure message is no help here, because a `Float` and a `Double` holding this value both print as `0.82`; only the box type differs.
code
java · 11 linesResponse response = get("/glossaries/ui-strings");
Object confidence = response.jsonPath().get("glossary.entry[0].confidence");
assertThat(confidence, instanceOf(Float.class));
response.then()
.body("glossary.entry[0].confidence", equalTo(0.82f))
.body("glossary.entry[0].reviewCount", equalTo(3));
float asFloat = response.jsonPath().getFloat("glossary.entry[0].confidence");
double asDouble = response.jsonPath().getDouble("glossary.entry[0].confidence");go deeper
Recall the one-letter fix: a JSON decimal arrives as a Float, so the literal in the matcher needs the f suffix. Know that whole numbers are not affected.
Explain the chain: the slurper produces a BigDecimal, the default FLOAT_AND_DOUBLE model narrows it to a Float, and equalTo compares with equals, which fails on the box type before it ever compares numbers.
Show the judgment call: decide the suite's number model once and document it, and argue for band assertions over exact decimals so a re-scored payload does not turn the suite red.
Own the consequence of changing the model. BIG_DECIMAL or BIG_INTEGER rewrites the type of every number in every covered response, so treat it as a migration with a blast radius, not a config tweak.
## What actually comes back from a JSON number REST Assured does not hand you the raw text of a JSON number, and it does not hand you a `double` either. The parse runs through `ConfigurableJsonSlurper`, a copy of Groovy's `JsonSlurper` that exists precisely to override the number model. Groovy's own slurper returns a `BigDecimal` for every non-integer literal; REST Assured then post-processes that value according to the configured `NumberReturnType`, whose default is `FLOAT_AND_DOUBLE`. Under that default the rule is: - A non-integer literal becomes a **`Float`**, unless its value is greater than `Float.MAX_VALUE`, in which case it becomes a **`Double`**. - A whole number stays an **`Integer`** or a **`Long`**, chosen by magnitude — no conversion is applied. - Nothing is rounded on the way in; the `BigDecimal` is simply narrowed by calling `floatValue()`. So for the glossary payload returned by `GET /glossaries/ui-strings`: ```json { "glossary": { "entry": [ { "term": "checkout", "confidence": 0.82, "reviewCount": 3 }, { "term": "cart", "confidence": 0.41, "reviewCount": 0 } ] } } ``` `glossary.entry[0].confidence` is a `Float` holding `0.82f`, and `glossary.entry[0].reviewCount` is an `Integer` holding `3`. ## Why the matcher says no `equalTo(0.82)` in Java is `equalTo(Double.valueOf(0.82))`, because `0.82` is a `double` literal. Hamcrest's `equalTo` is an equality matcher: it calls `expected.equals(actual)`. `Double.equals` returns `false` for anything that is not a `Double`, and `Float.equals` returns `false` for anything that is not a `Float`. The comparison never reaches the numeric values at all — it fails on type. The failure message prints both sides in a form that looks numerically identical, which is exactly why this trap costs people an afternoon: ``` JSON path glossary.entry[0].confidence doesn't match. Expected: <0.82> Actual: 0.82 ``` The narrowing is also lossy in a second, quieter way. `0.82` is not exactly representable in binary, and the nearest `float` and the nearest `double` are different numbers. Even if the types matched, comparing a narrowed value against a `double` literal for exact equality would be fragile. ## The four ways out, and when to use each | Fix | What you write | Use it when | |---|---|---| | Float literal | `body("...confidence", equalTo(0.82f))` | the default number model is fine and you want the smallest change | | Coercing getter | `jsonPath().getFloat(path)` / `getDouble(path)` | you are extracting into a variable rather than matching inline | | Range matcher | `body("...confidence", greaterThan(0.5f))` | the exact value is not the point of the test | | Change the model | `NumberReturnType.BIG_DECIMAL` | the payload carries values where precision genuinely matters | A few notes on those: 1. The typed getters on `JsonPath` coerce for you — `getDouble` and `getFloat` funnel whatever the parser produced through a converter, so they work no matter which number model is active. 2. A range matcher still needs the right box type: `greaterThan(0.5)` compares `Double`s and will not accept a `Float` any more than `equalTo` will. 3. Hamcrest's `closeTo(double, double)` is typed for `Double`, so it does not match a `Float` either — reaching for it is a common second mistake after the first one. 4. Switching to `BIG_DECIMAL` changes the type of every decimal in every response the configuration covers, so it is a suite-wide decision, not a per-assertion one. ## Practical guidance - If your assertions are about identity — an id, a count, a status — you will never meet this, because whole numbers are not converted. - If they are about measurements — a confidence score, a ratio, a duration — decide the number model once, write it into the shared configuration, and put a line in the team's README about it. - Prefer asserting a band over an exact decimal wherever the test's intent allows it. "Confidence is above 0.5" survives a re-scoring of the glossary; "confidence is exactly 0.82" does not. - When you do need exactness, `BIG_DECIMAL` with a `BigDecimal` built from a **string** is the honest spelling: `is(new BigDecimal("0.82"))`. Building one from a `double` reintroduces the binary representation you were trying to escape. The underlying lesson generalises past this library: a JSON number has no type in the document, so every parser picks one for you. Knowing which one your parser picked is part of knowing your tools.
- Does the same trap apply to a whole number such as reviewCount?No. Under the default number model whole numbers are left as `Integer`, or `Long` when they are too large, so `equalTo(3)` matches directly. It only bites if you switch the model to `BIG_INTEGER`, which converts those same `Integer` and `Long` results to `BigInteger` and breaks every `equalTo(3)` in the suite at once.
- Why does the failure message print both values as 0.82?The message renders each side with `toString()`, and a `Float` and a `Double` holding the nearest representable value to 0.82 both print as `0.82`. The mismatch is in the box type, which the message does not show. Inspecting the extracted value's class, or asserting with `instanceOf`, is the quickest way to see it.
saying these in an interview costs you the question
- Blaming floating-point rounding for what is a type mismatch
- Assuming a JSON decimal arrives as a Java double
- Reaching for closeTo, which is typed for Double and also fails
- Switching the whole suite to BIG_DECIMAL to fix one assertion
- Believing the failure message shows the actual runtime type