$json.generate() output breaks JSON.parse() when embedded in a JS single-quoted string literal (control character in string literal)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Problem Statement
$json.generate() (JSONTool) produces syntactically valid, correctly-escaped JSON — but the common VTL pattern used to consume it client-side, JSON.parse('$!json.generate($specs)'), breaks whenever any value in the generated JSON contains a character the JSON serializer escapes with a backslash sequence (\n, \r, \t, \", \\), or contains a raw single-quote character. Because the JSON text is embedded directly inside a JS single-quoted string literal, the browser's own JavaScript parser decodes those same escape sequences (JS also treats \n, \r, \t, \", \\ as string-literal escapes) before JSON.parse ever runs — turning valid JSON escape sequences into raw, literal control-character bytes. JSON.parse then rejects the string with Uncaught SyntaxError: Bad control character in string literal in JSON, since the JSON grammar forbids literal (unescaped) control characters inside string literals.
This is not specific to any one field type — any content field whose value can contain a newline, tab, or embedded quote (a Textarea, WYSIWYG field, multi-line Custom Field, etc.) will trigger it. It affects any VTL author using dotCMS's own $json Velocity tool for its documented purpose (producing JSON for front-end JS consumption), because dotCMS currently provides no safe way to embed $json.generate() output inside a <script> block for JSON.parse().
Identified via a support investigation into a VTL widget rendering a list pulled via $dotcontent.pull(). See linked Freshdesk ticket for the original report.
Steps to Reproduce
- In a dotCMS instance, identify or create any Content Type with at least one field whose stored value can contain a newline, tab, or embedded quote. In practice this is nearly unavoidable — even a content type's own built-in field metadata (e.g. a hidden "URL Title" custom field storing a multi-line VTL template) is enough to trigger this; no special field type or customer-authored content is required.
- Add a VTL page/widget containing:
#set($pulledContent = $dotcontent.pull("+contentType:<YourContentType>",10,"modDate desc")) var pulledContentVariable = JSON.parse('$!json.generate($pulledContent)'); - Load the page and open the browser console.
Expected: pulledContent is populated with the parsed JSON; no console error.
Actual: Uncaught SyntaxError: Bad control character in string literal in JSON at position N thrown by JSON.parse.
Confirmed via: Reproduced independently on two unrelated content types in two separate environments — the originally-reported content type, and a second, unrelated content type in an internal test environment — confirming this is not tied to a specific content type or field configuration. In both cases, extracting the exact byte at the reported failure position (via raw.charCodeAt(pos) on the JS string, before calling JSON.parse) showed a literal, unescaped control byte (code 13 / \r) sitting inside what dotCMS's server-side JSON serializer (com.dotmarketing.util.json.JSONObject.quote(), JSONObject.java:1159) had correctly escaped as a 2-character \r sequence server-side — confirming the corruption happens client-side during JS string-literal parsing, not in dotCMS's JSON generation.
Acceptance Criteria
- Add a new method to
JSONTool(e.g.generateEscaped(Object o)) that returns the same JSON produced bygenerate(Object), additionally escaped so it is safe to embed directly inside a single-quoted JS string literal forJSON.parse()— specifically: double every backslash so JSON's own escape sequences survive JS string-literal decoding intact, and escape any raw single-quote character so it can't terminate the JS string delimiter early. -
JSON.parse('$!json.generateEscaped($specs)')correctly parses regardless of embedded newlines, tabs, quotes, or backslashes anywhere in the underlying data (verified with a repro case containing at least one such character). - Existing
$json.generate()behavior/output is unchanged (this is an additive method, not a behavior change). - Unit test covering
generateEscaped()with a value containing\r\n,\",\\, and'. - Brief javadoc on the new method explaining when to use it vs.
generate().
dotCMS Version
26.08.03-01
Severity
Medium - Some functionality impacted
Links
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Locate JSONTool and its existing generate(Object) implementation, then compare its output with the escaping behavior noted in JSONObject.java:1159. Add the documented generateEscaped() behavior and a unit test using carriage returns, newlines, quotes, backslashes, and a single quote. Confirm the new output parses through JSON.parse while generate() remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100