api-client/packages/hoppscotch-common/src/helpers/editor/linting/jsonc.ts
James George d3144f99fb
fix: strip comments from JSON request bodies in CLI (#5769)
Fixes collections with JSON comments failing in the CLI with `SerializationException` while working fine in the app, where comments are stripped before sending requests, but the CLI was sending them as-is, breaking APIs like AWS Cognito that expect valid JSON.
2026-01-14 17:06:30 +05:30

108 lines
3 KiB
TypeScript

import { Node, parseTree, stripComments as stripComments_ } from "jsonc-parser"
import jsoncParse from "~/helpers/jsoncParse"
import { convertIndexToLineCh } from "../utils"
import { LinterDefinition, LinterResult } from "./linter"
const linter: LinterDefinition = (text) => {
try {
jsoncParse(text)
return Promise.resolve([])
} catch (e: any) {
return Promise.resolve([
<LinterResult>{
from: convertIndexToLineCh(text, e.start),
to: convertIndexToLineCh(text, e.end),
message: e.message,
severity: "error",
},
])
}
}
/**
* An internal error that is thrown when an invalid JSONC node configuration
* is encountered
*/
class InvalidJSONCNodeError extends Error {
constructor() {
super()
this.message = "Invalid JSONC node"
}
}
// NOTE: If we choose to export this function, do refactor it to return a result discriminated union instead of throwing
/**
* @throws {InvalidJSONCNodeError} if the node is in an invalid configuration
* @returns The JSON string without comments and trailing commas
*/
function convertNodeToJSON(node: Node): string {
switch (node.type) {
case "string":
return JSON.stringify(node.value)
case "null":
return "null"
case "array":
if (!node.children) {
throw new InvalidJSONCNodeError()
}
return `[${node.children
.map((child) => convertNodeToJSON(child))
.join(",")}]`
case "number":
return JSON.stringify(node.value)
case "boolean":
return JSON.stringify(node.value)
case "object":
if (!node.children) {
throw new InvalidJSONCNodeError()
}
return `{${node.children
.map((child) => convertNodeToJSON(child))
.join(",")}}`
case "property":
if (!node.children || node.children.length !== 2) {
throw new InvalidJSONCNodeError()
}
const [keyNode, valueNode] = node.children
// Use keyNode.value instead of keyNode to avoid circular references.
// Attempting to JSON.stringify(keyNode) directly would throw
// "Converting circular structure to JSON" error.
// If the valueNode configuration is wrong, this will return an error, which will propagate up
return `${JSON.stringify(keyNode.value)}:${convertNodeToJSON(valueNode)}`
}
}
function stripCommentsAndCommas(text: string): string {
const tree = parseTree(text, undefined, {
allowEmptyContent: true,
allowTrailingComma: true,
})
// If we couldn't parse the tree, return the original text
if (!tree) {
return text
}
// convertNodeToJSON can throw an error if the tree is invalid
try {
return convertNodeToJSON(tree)
} catch (_) {
return text
}
}
/**
* Removes comments from a JSON string.
* @param jsonString The JSON string with comments.
* @returns The JSON string without comments.
*/
export function stripComments(jsonString: string) {
return stripCommentsAndCommas(stripComments_(jsonString) ?? jsonString)
}
export default linter