DEV Community

MordecaiNilsson7582
MordecaiNilsson7582

Posted on

Malformed JSON from Moderation APIs: Node.js Handling Strategies for Healthtech CRM Actions

Malformed JSON from Moderation APIs: Node.js Handling Strategies for Healthtech CRM Actions

Short answer: parse the moderation API response once, validate it against a narrow command schema, and send malformed JSON or ambiguous actions to review instead of repairing and writing them.

A malformed moderation response should never become a CRM write. Treat the body as untrusted bytes, parse it once, validate its shape, normalize only known fields, and send ambiguity to review. For a healthtech sales-call summarizer, a wrong note or follow-up task can create a compliance problem long after the request has left the logs.

The example receives moderate user content, proposes CRM actions, and records them only after a schema check. I also record token and retry metadata per tenant. That makes the failure path inspectable and keeps cost decisions separate from parser folklore.

How should Node.js handle malformed JSON from a moderation API response?

A parser answers whether bytes can be decoded as JSON. It does not answer whether the value is an object, whether actions is an array, or whether an action targets a real contact. [], {"actions": null}, and an object with an unknown status can all be valid JSON and unsafe CRM input.

I initially treated parsing as the boundary. That was too small. In a notebook, printing a response and trying again feels harmless. In production, retries can duplicate spend, hide the original payload, and turn a formatting problem into inconsistent work. The useful failure unit is the proposed business action, not the HTTP request.

For a healthtech call, the safe states are accepted, rejected, or needs review. There is no implicit best-effort write. An uncertain summary can wait for an operator without silently changing an opportunity or attaching text to the wrong tenant.

The four gates before a CRM adapter are transport and content type, one parse, schema validation, and domain authorization.

The first gate checks transport and content type. The second parses exactly once. The third validates a small schema. The fourth applies domain rules: tenant identity, allowed action names, bounded text, and required identifiers.

This separation keeps recovery decisions local. A parse error can be retried under a strict budget. A schema error should usually be quarantined. A tenant mismatch should be rejected and audited. No exceptions.

Stop there.

Here is a small Node.js boundary. The CRM client is injected, and the endpoint remains generic. The parser does not repair malformed text; repair changes evidence needed during investigation.

function parseModerationPayload(rawBody, contentType) {
  if (!contentType.toLowerCase().includes("application/json")) {
    return { state: "needs_review", reason: "unexpected_content_type" };
  }

  let value;
  try {
    value = JSON.parse(rawBody);
  } catch {
    return { state: "needs_review", reason: "malformed_json" };
  }

  if (!value || Array.isArray(value) || typeof value !== "object") {
    return { state: "needs_review", reason: "payload_not_an_object" };
  }
  if (!Array.isArray(value.actions)) {
    return { state: "needs_review", reason: "actions_not_an_array" };
  }
  return { state: "parsed", value };
}
Enter fullscreen mode Exit fullscreen mode

The result is not a boolean. It gives the caller enough information to retry, hold, or reject without parsing twice. That distinction matters when a worker receives a response that starts with a valid object and ends at a truncated string: retrying from the handler can be reasonable, but passing a partly recovered object to a CRM writer is not. The raw response, content type, tenant context, retry count, and validation reason should travel together as an internal event, while sensitive call text stays behind the retention policy. In a real service, cap captured bodies, redact call content under the tenant policy, and attach a correlation ID instead of logging sensitive text freely.

How do schema gates protect the action, not just the JSON?

Validation should produce an internal object narrower than the model response. The CRM writer may accept only a tenant ID from request context, an allow-listed action kind, a stable record ID, and bounded text. The model must not choose the tenant.

For a narrow contract, a small validator can be enough when it is tested as a contract. For larger schemas, use a JSON Schema validator and version the schema with the command consumer. In both cases, reject unknown action kinds and missing identifiers before a side effect.

const allowedKinds = new Set(["create_follow_up", "add_call_note"]);

function toCommands(payload, requestContext) {
  const commands = [];
  for (const action of payload.actions) {
    if (!action || typeof action !== "object") return null;
    if (!allowedKinds.has(action.kind)) return null;
    if (action.tenantId !== requestContext.tenantId) return null;
    if (typeof action.recordId !== "string" || action.recordId.length === 0) return null;
    if (typeof action.text !== "string" || action.text.length > 2000) return null;
    commands.push({ kind: action.kind, tenantId: requestContext.tenantId,
      recordId: action.recordId, text: action.text });
  }
  return commands;
}
Enter fullscreen mode Exit fullscreen mode

This is a real trade-off: a false rejection costs a review step, while a false acceptance creates a side effect with incomplete provenance. Make thresholds visible in code and metrics. I use a 2,000-character text limit and one retry as starting constraints, then change them only when replay results justify it. A malformed response should not consume an unbounded retry loop.

Recovery is a state machine, not string repair

For moderate content, use a small retry budget and an idempotency key derived from call and tenant context. A parse failure may trigger one constrained regeneration if the tenant remains within budget. The original response stays as evidence for the first failure. This is response handling, not string repair.

After the retry, the path is deterministic:

  • valid payload and commands: enqueue an idempotent CRM operation;
  • valid JSON with an invalid schema: hold for review;
  • malformed JSON after the budget: hold with the failure reason;
  • tenant or authorization mismatch: reject, audit, and do not regenerate blindly.

The queue consumer repeats the idempotency check. An HTTP-handler gate alone does not protect against duplicate delivery, worker restart, or operator replay. A command ID should make repeated delivery harmless.

This is where notebook-to-prod habits matter. Before wiring the adapter, run the same contract against fixtures containing truncated JSON, a JSON string instead of an object, duplicate actions, overlong text, and a valid response with the wrong tenant. The fixture set is small. The coverage is not.

What should be measured before copying this pattern?

Measure malformed responses by tenant, schema rejections by action kind, retry count, review queue age, duplicate suppression, and accepted actions later reversed by an operator. Record input and output token counts, retry cause, and latency in a tenant-scoped ledger, excluding or redacting content as policy requires. One aggregate rate can hide a noisy tenant or a prompt revision that doubled retries.

Evaluate the prompt and validator together. A stricter schema can raise review volume while reducing unsafe writes; that is not automatically a regression. Build a replay harness from sanitized fixtures, score final commands rather than prose alone, and make every schema change pass those cases before deployment.

Parsing makes data readable; validation makes it eligible; authorization and idempotency make a side effect safe to attempt. Keep those decisions separate, and malformed output becomes a review event instead of a mysterious CRM mutation.

Further reading

Top comments (0)