Skip to content

fix(coerce): unwrap nested stringified JSON objects to satisfy schema - #1095

Merged
murdore merged 1 commit into
releasefrom
fix/coerce-deep-unwrap-nested-json-strings
Jun 13, 2026
Merged

murdore merged 1 commit into
releasefrom
fix/coerce-deep-unwrap-nested-json-strings

Conversation

@murdore

@murdore murdore commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Problem

Models sometimes double-encode a nested field as a JSON string — {"attachment":"{...}"} instead of {"attachment":{...}} — which fails schema validation even though the intended object is right there.

Observed in production (Tara's {summary, attachment} envelope): a structured response came back with attachment as a JSON string, so the declared file type (pdf) was lost and the content degraded to a markdown/text fallback. coerceJsonToSchema already unwraps a stringified top-level object (the JSON-string-literal case), but not a stringified nested field.

Fix

When a parsed candidate fails schema.safeParse, run a depth-bounded deepUnwrapJsonStrings pass that parses any string-valued field whose content is itself a JSON object/array, then re-validate:

  • The safeParse gate is the safety net — an over-eager unwrap (a field that should stay a string) fails re-validation and is rejected, falling back to prior behaviour.
  • A parsed string is not re-descended into, so an attachment's own string content (even if it looks like JSON) is preserved verbatim.
  • Only runs when a schema is supplied (no schema → unchanged first-parseable-wins behaviour); recursion is depth-bounded.

This generalizes to any double-encoded nested field, for every NeuroLink consumer — not just curator.

Test

npx tsx test/continuous-test-suite-coerce-nested-unwrap.ts   # 6/6 PASS
npx tsx test/continuous-test-suite-structured-coerce.ts      # 7/7 PASS (no regression)

Covers: the production attachment-as-string case, content preservation (no re-descent), clean passthrough, null attachment, the over-unwrap gate (JSON-looking string kept as string), and the no-schema boundary. tsc clean; eslint clean.

Follow-up to the structured-output line of fixes (#1088 top-level unwrap, #1092 responseSchema sanitize, #1093 400-abort). Curator picks this up on the next @juspay/neurolink bump.

Models sometimes double-encode a NESTED field as a JSON string —
{"attachment":"{...}"} instead of {"attachment":{...}} — which fails schema
validation even though the intended object is present. Observed in production:
a structured {summary, attachment} response came back with attachment as a JSON
string, so the declared file type (pdf) was lost and the content degraded to a
markdown/text fallback.

coerceJsonToSchema already unwraps a stringified TOP-LEVEL object; this extends
it to nested fields. When a parsed candidate fails schema.safeParse, string
fields whose value is itself a JSON object/array are parsed (deepUnwrapJsonStrings)
and the result is re-validated:
- The safeParse gate is what makes it safe — an over-eager unwrap (a field that
  should stay a string) fails re-validation and is rejected, falling back to the
  prior behaviour.
- A parsed string is NOT re-descended into, so an attachment's own string
  content (even if it looks like JSON) is preserved verbatim.
- Recursion is depth-bounded; only runs when a schema is supplied (no schema =
  unchanged first-parseable-wins behaviour).

Deterministic suite added; existing structured-coerce suite still green.
Copilot AI review requested due to automatic review settings June 13, 2026 17:38
@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
neurolink Ready Ready Preview, Comment Jun 13, 2026 5:38pm

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@murdore, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 6 minutes and 17 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3b193808-fa17-4450-960f-012157cdd9d4

📥 Commits

Reviewing files that changed from the base of the PR and between 1e5b156 and f3c0f6e.

📒 Files selected for processing (2)
  • src/lib/utils/json/coerce.ts
  • test/continuous-test-suite-coerce-nested-unwrap.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/coerce-deep-unwrap-nested-json-strings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Single Commit Policy - COMPLIANT

Status: Policy requirements met • 1 commit • Valid format • Ready for merge

📊 View validation details

📝 Commit Details

  • Hash: f3c0f6e8fb8d22c84ceecc33350fb0e1a6ffa8eb
  • Message: fix(coerce): unwrap nested stringified JSON objects to satisfy schema
  • Author: Sachin Sharma

✅ Validation Results

  • Single commit requirement met
  • No merge commits in branch
  • Semantic commit message format verified
  • Ready for squash merge to release branch

🤖 Automated validation by NeuroLink Single Commit Enforcement

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@Tara-ag Tara-ag left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Files reviewed: 2
New issues: 0 (all clear)

Analysis

This PR addresses a production issue where models double-encode nested fields as JSON strings (e.g., {"attachment": "{...}"} instead of {"attachment": {...}}). The fix is well-architected with multiple safety layers:

src/lib/utils/json/coerce.ts:

  • ✅ Depth-bounded recursion (MAX_NESTED_UNWRAP_DEPTH = 6) prevents stack overflow on pathological inputs
  • ✅ Schema re-validation gate prevents over-unwrapping fields that should remain strings
  • ✅ No re-descent into parsed objects preserves intended string content (e.g., file content)
  • ✅ Immutable implementation returns new values, never mutates input
  • ✅ repaired: true correctly signals when transformation occurred

test/continuous-test-suite-coerce-nested-unwrap.ts:

  • ✅ Covers the production bug case (Tara's attachment envelope)
  • ✅ Validates content preservation (no re-descent)
  • ✅ Tests the over-unwrap safety gate
  • ✅ Includes null handling and no-schema boundary cases
  • ✅ Follows established test harness patterns

Verification Against Standards

  • CLAUDE.md Rule compliance: No violations detected
  • Security: No hardcoded secrets, safe JSON parsing with try/catch
  • Type safety: Proper unknown handling, no any casts
  • Backward compatibility: No breaking changes to public API

The implementation elegantly solves the nested double-encoding problem while maintaining defensive programming practices. Approved for merge.

@murdore
murdore merged commit 2f0b856 into release Jun 13, 2026
17 of 18 checks passed
@murdore
murdore deleted the fix/coerce-deep-unwrap-nested-json-strings branch June 13, 2026 17:46
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 9.70.7 🎉

The release is available on:

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
Preview — f3c0f6e8 Deployed Jun 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants