# Validating a code

  Check a code where it enters your system, rather than where it leaves. By the time a claim is
  rejected for an unrecognised code, the patient has gone home and the correction costs a person
  rather than a round-trip.

## Two questions that look like one

"Is this code valid?" is two questions, and they have different endpoints because they have
different answers.

<Wire
  rows={[
    { call: "Does this code exist in this system, and is it active?", does: "A question about the vocabulary. NATIONAL_ID is a real identifier type. NATIONAL-ID, with a hyphen, is not.", keep: "$validate-code on the code system" },
    { call: "Is this code allowed in this field?", does: "A question about the binding. A withdrawn identifier type still exists in the vocabulary. It is simply not offered any more, and existing in the vocabulary is not the same as being allowed in the field.", keep: "$validate-code on the value set" },
  ]}
/>

The second is almost always the one you want on a form. A field is bound to a value set, and the
question at the boundary is whether what arrived belongs in that set, not whether it exists
somewhere in the wider vocabulary.

  `$validate-code` answers `200` whether the code is valid or not. The verdict is the `result`
  parameter inside the FHIR `Parameters` body. An integration that branches on the status code
  treats every invalid code as accepted, and it will pass every test you write against valid
  input.

## What the answer looks like

Both operations return a FHIR `Parameters` resource. Three parameters matter:

<Wire
  rows={[
    { call: "result", does: "The verdict, as a boolean. This is the only field your branch should read.", keep: "result" },
    { call: "display", does: "The label, when the code resolved. Useful to render back at the operator so they can see what they matched.", keep: "nothing" },
    { call: "message", does: "Why it failed, when it did. Log it: 'invalid code' with no reason is a support ticket rather than a fix.", keep: "message" },
  ]}
/>

## Where each check belongs

<Wireframe
  title="Add a diagnosis"
  actor="Clinician"
  rows={[
    [{ label: "Diagnosis code", from: "1A00", kind: "field", span: 2 },
     { label: "Cholera", note: "Resolved, with the display returned by the service", tone: "positive", span: 2 }],
    [{ label: "Diagnosis code", from: "1AOO", kind: "field", span: 2 },
     { label: "Not a code in ICD-11", note: "Letter O typed for a zero. Caught here, not at submission.", tone: "negative", span: 2 }],
    [{ label: "Save diagnosis", kind: "action", span: 4 }],
  ]}
  caption="Both calls returned 200. The difference is entirely inside the body, which is why the status code is the wrong thing to branch on."
/>

<Mermaid chart={`sequenceDiagram
    autonumber
    participant U as Clinician
    participant H as Your HMIS
    participant T as Terminology service

    U->>H: Types a code, leaves the field
    H->>T: GET /value-sets/{short code}/$validate-code/?code=1A00&version=serving
    T-->>H: 200 · Parameters { result: true, display: "Cholera" }
    Note over H: Read result, not the status.
    H-->>U: Shows "Cholera" beside the field

    U->>H: Types 1AOO
    H->>T: Same call
    T-->>H: 200 · Parameters { result: false, message: "..." }
    Note over H: Still a 200.
    H-->>U: Marks the field, keeps the message in the log
`} />

<Mermaid chart={`flowchart TD
    A["A code enters your system"] --> B{"From where?"}
    B -- "An operator typed it" --> C["Validate against the bound value set<br/>as the field loses focus"]
    B -- "Picked from a fetched list" --> D["Already valid at fetch time,<br/>but the list may be stale"]
    B -- "An interface or import" --> E["Validate every row.<br/>This is the path nobody checks."]
    C --> F{"result true?"}
    D --> G["Check your cache version<br/>against the serving version"]
    E --> F
    F -- No --> H["Reject at the boundary,<br/>log the message"]
    F -- Yes --> I["Store the code and the version"]
    G -- Stale --> J["Re-sync, then revalidate<br/>anything not yet submitted"]
    G -- Current --> I
`} />

## The calls

<ApiRef method="GET" path="/value-sets/{short code}/$validate-code/?code={code}&version=serving" to="/terminology-api/fhir-operations" keep="result">
  Is this code a member of this value set? The binding check for a form field, and the one you
  want most of the time.
</ApiRef>

<ApiRef method="GET" path="/code-systems/{system}/$validate-code/?code={code}&version=serving" to="/terminology-api/fhir-operations" keep="result">
  Does this code exist in this system, and is it active? A question about the vocabulary rather
  than the field.
</ApiRef>

<ApiRef method="GET" path="/code-systems/{system}/$lookup/?code={code}" to="/terminology-api/fhir-operations" keep="display">
  What does this code mean? Display, definition, designations and properties. This is how you
  render a stored code you never stored a label for.
</ApiRef>

  Storing the display text alongside the code feels like a saving and is a liability: when the
  wording is corrected upstream, your records now disagree with the vocabulary and you cannot tell
  which of the two is right. Store the code and the version you read it at, and resolve the label
  with `$lookup` when you need to render it.

## Translating rather than coding twice

Some payers are still on ICD-10 while diagnoses are coded in ICD-11. The answer is not to store
both.

<ApiRef method="GET" path="/concept-maps/{short code}/$translate/?code={code}" to="/terminology-api/concept-maps" keep="the target code">
  The equivalent code in another vocabulary, one match per target.
</ApiRef>

Code once in the primary vocabulary, and let the map resolve the rest. A system that stores the
older representation as its primary one has committed to a migration it has not scheduled.

There is more to this than the one call. Which map to use, what an equivalence promises, and why
a `result: false` means two different things are covered in
[translating between vocabularies](/rail/reference/Terminology-Concept-Maps).

## Building the check in

<BuildSequence
  steps={[
    {
      do: "Validate at the boundary, not at submission",
      detail:
        "Every place a code can enter: a typed field, an import, an interface from a third-party system. The import is the one that is usually missed, and it is the one that arrives at volume.",
    },
    {
      do: "Read the result parameter, never the status code",
      detail:
        "Write this once, in one helper, so no call site can get it wrong. A helper that returns a boolean from the body is the whole fix.",
    },
    {
      do: "Pin the check to the same version as the picker",
      detail:
        "Validating against serving while your dropdown was populated from a cached older release will reject codes you offered. One version string, read in both places.",
    },
    {
      do: "Log the message on failure",
      detail:
        "The reason is in the body. Discarding it turns every invalid code into the same unhelpful line in your log.",
    },
    {
      do: "Do not validate in a loop over a large import",
      detail:
        "Fetch the value set expansion once and check membership locally, then validate individually only what failed. One call per row of a ten thousand row import is a self-inflicted outage.",
    },
  ]}
/>

## What breaks here

<CostTable
  rows={[
    {
      where: "Submission, in production only",
      symptom: "\"Invalid codes are getting through and we validate everything\"",
      cause:
        "The check branches on the HTTP status, which is 200 for an invalid code. Read the result parameter.",
    },
    {
      where: "A form the user filled correctly",
      symptom: "\"It rejects a code we offered in our own dropdown\"",
      cause:
        "The picker and the validator are reading different versions. Pin both to the same one.",
    },
    {
      where: "A bulk import",
      symptom: "\"The import takes hours, or times out\"",
      cause:
        "One validation call per row. Expand the value set once and check membership in memory.",
    },
    {
      where: "Rendering an old record",
      symptom: "\"The label we show does not match the payer's statement\"",
      cause:
        "A display string stored at capture time and never revisited. Store the code, resolve the label.",
    },
    {
      where: "A third-party interface",
      symptom: "\"Codes from the lab system are rejected downstream\"",
      cause:
        "Validation was built on the typed-entry path only. Every entry point needs the same boundary check.",
    },
  ]}
/>

**Next:** [Translating between vocabularies](/rail/reference/Terminology-Concept-Maps) ·
[Pinning to a release](/rail/reference/Terminology-Pinning-Versions) ·
[Error dictionary](/rail/reference/Errors)
