All articles
169 articles · updated weekly See our Tools
All articles
Tutorials

JSON Schema: Why You Get 'must be number' and How to Fix It for Good

The 'must be number' error from JSON Schema has four real root causes in production. Understand why it happens, how to read the instancePath, and why coerceTypes is the wrong answer.

JSON Schema: Why You Get 'must be number' and How to Fix It for Good
COVER · Tutorials

The payload arrived. The JSON parses without error. The schema is declared. And yet the validator returns must be number for a field that clearly contains a number. The first reaction is to suspect the schema. The second is to suspect the validator. Neither is right.

The must be number error in JSON Schema isn't a validator bug — it's the validator doing exactly what it should: rejecting data that violates the contract. The problem is with whoever sent the data.


Where the "must be number" error comes from in practice

There are four sources that generate this error with consistent enough frequency to warrant a list.

1. HTML forms

A <form> with method="POST" without Content-Type: application/json sends application/x-www-form-urlencoded by default. All fields arrive as strings on the server, without exception. The amount field with value 150 in the form arrives as "150" in the payload. This isn't a browser bug — it's the specified behavior.

The problem appears when the backend receives this form data, constructs a JavaScript/Python object without converting types, and tries to validate against a schema with "type": "number". Validation fails because the string "150" is not the number 150.

2. APIs that serialize numbers as strings

This isn't uncommon. Legacy systems frequently return financial fields as strings: "amount": "150.00" instead of "amount": 150.00. Sometimes it's a design decision to preserve decimal precision (floating-point and money is a long discussion). Sometimes it's carelessness. The result for the consumer is the same: string instead of number.

If you're validating a third-party API response against a schema, this is the most likely case.

3. Databases that serialize DECIMAL as string

ORMs and database drivers have varied behavior with DECIMAL and NUMERIC types. Depending on the driver and configuration, a DECIMAL(10,2) field may arrive as the string "150.00" in the object returned by the query. MySQL and PostgreSQL have different behaviors here, and some drivers change behavior by version.

The data was correct in the database. The serialization introduced the type conversion.

4. Environment variables

process.env.RATE_LIMIT is always a string, even if you wrote RATE_LIMIT=100 in .env. os.getenv('RATE_LIMIT') in Python, $_ENV['RATE_LIMIT'] in PHP — same. If you take a value from an environment variable and put it in an object that will be validated with JSON Schema, it's a string, not a number.

This case appears in service configuration: a schema that validates configuration read from environment variables will fail on every numeric field if you don't explicitly convert.


The difference between "type": "number" and "type": "integer"

JSON Schema has two numeric types, and the confusion between them generates a specific type of error.

"type": "number" accepts any JSON numeric value: 1, 1.5, 1.0, -42, 3.14. It does not accept the string "1".

"type": "integer" accepts only values without a fractional part: 1, 42, -10. It does not accept 1.5. And a counterintuitive point: 1.0 is accepted as integer because mathematically it's an integer — there's no fractional part. JSON Schema evaluates the mathematical value, not the textual representation.

The most common confusion: using "type": "integer" for a monetary value field that can have cents. The schema rejects 19.99 because it's not an integer. The solution is "type": "number" — or, depending on the required precision, representing monetary values as integer cents (already an API design decision).

To test behavior before integrating the schema in code, paste the payload and schema in the JSON Schema Validator — it shows the error with the exact instancePath and which keyword failed.


How to read the instancePath to find the culprit field

AJV returns an array of errors, each with this structure:

{
  "instancePath": "/items/0/price",
  "schemaPath": "#/properties/items/items/properties/price/type",
  "keyword": "type",
  "params": {
    "type": "number"
  },
  "message": "must be number"
}

The instancePath is a JSON Pointer — each segment separated by / indicates a level in your data structure. /items/0/price means: inside the items array, in the first element (index 0), the price field.

If the instancePath is empty (""), the error is at the root object, not a nested field.

The schemaPath shows where in the schema the failed keyword is — useful when the schema uses $ref or allOf and the actual source of the rule isn't immediately obvious.

With allErrors: true in AJV, you receive all errors at once. Without it, the validator stops at the first — which is frustrating when there are multiple wrong fields in the payload.


coerceTypes: the shortcut that becomes technical debt

AJV has the coerceTypes: true option. With it enabled, the string "150" passes the "type": "number" validation — the validator silently converts.

The temptation is obvious: the error disappears, the tests pass, move on.

The problem: the data producer continues sending strings. You didn't fix the contract — you made the consumer accept wrong data. Six months from now, someone will use this field in a math operation without knowing it can arrive as a string in some code path that doesn't go through the validator with coercion. The bug will reappear, harder to track because the schema "says" the field is a number but in practice accepts strings.

Coercion is useful in two specific cases: URL query strings (everything arrives as string by definition and converting is reasonable) and environment variable configuration (where you control the schema and the conversion is explicit and documented). Outside of this, coercion masks the bug — it doesn't fix it.

The right fix is to convert the type in the right place: in the form data parser, in the third-party API adapter, in the query result mapper. Where data enters the system, not where it's validated.


When numeric string is a design decision

Sometimes you don't control the producer. The third-party API returns "amount": "150.00" and it won't change. In that case, the schema should reflect reality:

{
  "amount": {
    "type": "string",
    "pattern": "^-?[0-9]+(\\.[0-9]+)?$"
  }
}

Or, if the field can be string or number depending on the version of the API you're consuming:

{
  "amount": {
    "oneOf": [
      { "type": "number" },
      { "type": "string", "pattern": "^-?[0-9]+(\\.[0-9]+)?$" }
    ]
  }
}

But explicitly document that this is a concession to the producer, not the ideal contract. When you change providers or the API is fixed, the schema should be updated to accept only "type": "number".

Union type with numeric string in a schema you control end-to-end is a sign that some side of the system isn't fulfilling the contract. It's worth investigating before formalizing the deviation in the schema.


Fix the producer, not the validator

The must be number error has a consistent root cause: something in the data pipeline is producing the wrong type. The form data that isn't parsed, the API that serializes differently from what's documented, the ORM that silently converts, the environment variable that wasn't converted.

The JSON Schema validator is doing its job when it rejects this data. The correct response is to investigate instancePath, identify the field, trace where it came from in the system, and convert the type at the input — not loosen the schema to accept wrong data.

The schema's function is to define the contract. When the contract needs to bend to accept incorrect data, the contract has lost its value.


Note: the editorial content ends here. What follows is a recommendation for a tool related to the post's topic.


To reproduce the error before writing code — paste the problematic payload and the schema in the JSON Schema Validator. The result shows the exact instancePath, the failed keyword, and the full AJV message. Especially useful when the schema has nested structures and the culprit field isn't obvious in the stack trace.

RD
Author
Rafael Duarte
Backend developer with a background in fintech and B2B SaaS — worked on teams that scaled APIs from zero to millions of requests. Carries enough production scars to hold strong opinions on tools, patterns and architecture decisions. Not an academic: he read the UUID RFC when he had to choose between v4 and v7 for a write-heavy table.
View profile