A Number Comes Back as a String: Validating Structured Output Types

July 24, 2026 · Claude Certified Architect (CCA) Prep (part 35)

▶ Watch on YouTube & subscribe to The Stack Underflow

Your extraction pipeline works. The JSON comes back, the keys are right, the values look correct at a glance — and then somewhere downstream, amount + tax throws, or silently concatenates two strings into "42.9910" instead of adding to 52.99. Nobody notices until a report is wrong by an order of magnitude. The model didn’t fail to extract the number. It extracted the number and handed it back as text, and nothing in the pipeline was set up to catch that before it did damage.

The one-sentence version: when a field comes back the wrong type, the fix isn’t a downstream cast — it’s a JSON schema with the right type on that field, plus a validator that rejects the response before your code ever sees it.

The question

You’re extracting structured data from documents. The amount field is supposed to be numeric, but the model keeps returning it as a string — "42" instead of 42. What’s the best fix?

  • A) Cast it downstream
  • B) Tighten the JSON schema (type: number) and validate the result
  • C) Ask more firmly
  • D) Lower max_tokens

Pause here and pick an answer before you scroll — this is CCA Domain 4 (Prompt Engineering & Structured Output, 20% of the exam), and the exam rewards knowing why an option is right, not just which letter it is.

The answer

B — tighten the JSON schema so amount is declared type: number, and run a validator against the response before it moves downstream.

The mechanism is simple, and it’s the same mechanism behind most of Domain 4: a schema isn’t a suggestion the model reads and does its best with — it’s the shape of the tool call itself. When you define a tool (or a response_format-style schema) with input_schema and force the model to call it, the model is filling in a structure whose field types you declared. If you left amount as an untyped or loosely-typed field, the model has room to hand back "42", "42.00", "$42", or 42 — all “look right” to a human skimming the output, all different types to a parser. Tightening the schema removes that room. Validating the result closes the loop: even a well-typed schema can occasionally drift, so a gate that checks the response before it’s trusted is what turns “usually correct” into “provably correct.”

BEFORE — loose schema, hope-based extraction
┌─────────────────────────────┐
│ "amount": { }                │   no type constraint
└─────────────────────────────┘


   model returns  "amount": "42"   ← string, looks fine, breaks math


   downstream code: amount + tax  →  TypeError or silent string concat

AFTER — typed schema + validation gate
┌─────────────────────────────┐
│ "amount": { "type": "number" }│  ← the guardrail lives HERE
└─────────────────────────────┘


   model returns  "amount": "42"   ← violates declared type


   VALIDATOR ──✗ reject, do not pass downstream

         └─▶ retry with error fed back: "amount must be type number, got string"


                model returns  "amount": 42   ← ✓ passes gate → downstream

The rule that pins this down for the exam: the schema types ARE the guardrail — don’t post-parse and hope. A type declaration on the field is a constraint the model call is validated against; a cast you apply after the fact is just cleanup for a mistake you already let through.

Why the other options fail

  • A) Cast it downstream. This treats the symptom, not the cause. A cast on amount might handle "42", but it silently swallows the next bad response too — "42, approx", null, or an empty string — because there’s no gate rejecting the malformed value before your business logic runs. You’ve turned a detectable contract violation into an undetected bug that shows up as wrong math three functions later. It also means every consumer of this data has to remember to cast it, instead of the guarantee living in one place.
  • C) Ask more firmly. Rewording the prompt — “please make sure amount is a number,” in all caps if you’re feeling desperate — is exactly the “prompt as hope” pattern this whole domain is built to break you of. Natural-language emphasis can nudge probability, but it does not constrain the output the way a declared schema type does. It’s non-deterministic where you need determinism.
  • D) Lower max_tokens. This has nothing to do with field typing. Lowering max_tokens truncates the response — it can cause malformed JSON by cutting output off mid-object, but it does nothing to make a completed amount field come back as the right primitive type. It’s a distractor built to sound like a “constrain the model” answer while acting on the wrong dimension entirely.

The concept behind it

This question is a narrow instance of a pattern that shows up across all of Domain 4: structured output reliability is a two-part system, not a single step. Part one is the schema — the tightest possible type constraints on every field (number, not string; an enum, not free text; required, not optional if the field must always be present). Part two is validation — a gate, external to the model call, that checks the actual response against that schema and rejects anything that doesn’t match before it reaches business logic.

Skipping part one and relying only on part two means you’re validating against a loose contract — you’ll catch fewer errors because your schema didn’t rule much out to begin with. Skipping part two and relying only on part one means you’re trusting the model to always honor the schema perfectly, every call, forever — and while forced tool-calling makes conformance far more likely, “far more likely” is not “guaranteed,” especially across model versions, edge-case inputs, and long-tail formatting quirks. You want both: a type-correct schema, and a validator that treats every response as unverified until it passes.

The other half of this pattern — what happens when validation fails — is its own exam-relevant question: you don’t retry forever, and you don’t retry blind. You cap the attempts and feed the specific validation error back into the next prompt, so the model is correcting a named mistake instead of guessing again. That’s covered in full in Your Retry Loop Never Stops: Bounded Retries for LLM Output.

Type validation also intersects with two adjacent failure modes worth knowing by name. If the schema is right but the model invents a value for a field it couldn’t actually find, that’s not a typing problem — allowing an explicit “unknown” or null in the schema is the fix, covered in Preventing Made-Up Output. And the entire reason any of this matters is usually that the output isn’t for a person to read — it’s for a program to parse, which is the framing in The Output Feeds a Program, Not a Person. Type-correct fields, honest nulls, and a bounded retry loop are the same discipline applied at three different points in the pipeline.

Failure modeWrong instinctCorrect fix
Number returns as stringCast it in application codetype: number in the schema + validate
Model invents a missing valueHope it doesn’t happenAllow null/“unknown” in the schema
Validation failsRetry forever, or give up on first tryBounded retries, error fed back into the next prompt
Output must be machine-parseableAsk nicely for clean formattingForce structure via tool_choice + schema

FAQ

Why does Claude return a number as a string in the first place? Without a strict type declaration, the model is free to produce whatever textual representation of the value it judges reasonable — and text generation naturally biases toward string-like output, especially when the surrounding context (a document, a line item, a receipt) presented the number as text. A declared type: number on the schema field removes that ambiguity from the call itself, rather than relying on the model to infer your intent.

Isn’t validating the output redundant if I already forced a schema with tool_choice? No — forced tool-calling with a schema makes conformance far more likely, not guaranteed. Model behavior can still drift on edge cases, unusual inputs, or across model version updates. Validation is the layer that turns “very likely correct” into “verified before it’s trusted,” and it’s also what gives you a concrete error message to feed into a retry loop when something does slip through.

Does this apply to nested fields, not just top-level ones? Yes — the same principle applies at every level of a JSON schema. A nested object’s numeric field, an array item’s enum, a date string inside a larger record: each needs its own type constraint, and a thorough validator checks the whole tree, not just the top-level keys.

What’s the difference between schema validation and business-logic validation? Schema validation checks shape and type — is amount a number, is status one of the allowed enum values. Business-logic validation checks meaning — is this amount plausible for this vendor, is this date in the future when it shouldn’t be. Schema validation should always run first; it’s cheaper, and there’s no point checking business rules against a field that isn’t even the right type yet.

Not affiliated with, authorized, or endorsed by Anthropic. “Claude” and “Claude Certified Architect” are trademarks of Anthropic. These are original practice questions for study, not real exam content — verify current exam details on Anthropic’s official pages.

Where this fits in the CCA series

This is Domain 4, question 2 of 10 — right after How Do You Guarantee Valid JSON? Forcing Structured Output, which covers the forced-tool_choice half of the contract this question assumes. Once the shape is forced and the types are validated, the next questions in the set cover keeping chain-of-thought out of the final structured payload (Keep the Reasoning, Hide It From Output) and building bounded retry loops for when validation fails (Your Retry Loop Never Stops). For the full five-domain map this series is built on, start with the Claude Certified Architect (CCA) Exam Guide. Browse all tutorials for the rest of the series.

Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.

Subscribe on YouTube →