refactor: consolidate safe json parsing (#99688)

This commit is contained in:
Dallin Romney
2026-07-04 16:33:27 -07:00
committed by GitHub
parent 4682364fa6
commit cf14b27d81
7 changed files with 34 additions and 41 deletions
+1
View File
@@ -2,6 +2,7 @@
export * from "./boolean-coercion.js";
export * from "./error-coercion.js";
export * from "./json-coercion.js";
export * from "./number-coercion.js";
export * from "./record-coerce.js";
export * from "./string-coerce.js";
@@ -0,0 +1,12 @@
import { describe, expect, it } from "vitest";
import { safeParseJson } from "./json-coercion.js";
describe("json-coercion", () => {
it.each<[string, unknown]>([
['{"ok":true}', { ok: true }],
["[1]", [1]],
['"text"', "text"],
["null", null],
["{", undefined],
])("parses %s", (value, expected) => expect(safeParseJson(value)).toEqual(expected));
});
@@ -0,0 +1,8 @@
/** Parses JSON without throwing, returning undefined for invalid input. */
export function safeParseJson(value: string): unknown {
try {
return JSON.parse(value) as unknown;
} catch {
return undefined;
}
}
+6 -15
View File
@@ -1,5 +1,7 @@
/** SQLite-backed ACP session metadata storage keyed through session-store entries. */
import type { DatabaseSync } from "node:sqlite";
import { safeParseJson } from "@openclaw/normalization-core";
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import type { Insertable, Selectable } from "kysely";
import { getRuntimeConfig } from "../../config/config.js";
@@ -29,7 +31,6 @@ import {
type OpenClawStateDatabaseOptions,
runOpenClawStateWriteTransaction,
} from "../../state/openclaw-state-db.js";
import { isRecord } from "../../utils.js";
/** ACP metadata joined with its legacy session-store row and config context. */
export type AcpSessionStoreEntry = {
@@ -89,21 +90,11 @@ function getAcpSessionKysely(db: DatabaseSync) {
return getNodeSqliteKysely<AcpSessionMetaDatabase>(db);
}
function parseOptionalJsonRecord(raw: string | null): Record<string, unknown> | undefined {
if (raw == null || raw === "") {
return undefined;
}
try {
const parsed = JSON.parse(raw) as unknown;
return isRecord(parsed) ? parsed : undefined;
} catch {
return undefined;
}
}
function rowToAcpSessionMeta(row: AcpSessionRow): SessionAcpMeta {
const identity = parseOptionalJsonRecord(row.identity_json) as SessionAcpIdentity | undefined;
const runtimeOptions = parseOptionalJsonRecord(row.runtime_options_json) as
const identity = asOptionalRecord(safeParseJson(row.identity_json ?? "")) as
| SessionAcpIdentity
| undefined;
const runtimeOptions = asOptionalRecord(safeParseJson(row.runtime_options_json ?? "")) as
| AcpSessionRuntimeOptions
| undefined;
return {
@@ -1,6 +1,8 @@
/**
* Detects message-tool sends that delivered a visible reply to the current source.
*/
import { safeParseJson } from "@openclaw/normalization-core";
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js";
import {
isMessageToolConversationCreateActionName,
@@ -70,14 +72,7 @@ function isBareSentDeliveryStatus(value: unknown): boolean {
}
function parseJsonRecord(value: string): Record<string, unknown> | undefined {
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: undefined;
} catch {
return undefined;
}
return asOptionalRecord(safeParseJson(value));
}
function recordHasDeliveredMessageId(record: Record<string, unknown>): boolean {
+2 -5
View File
@@ -3,6 +3,7 @@
*
* Accepts provider-specific tool-call and tool-result shapes used by transcript repair and announce capture.
*/
import { safeParseJson } from "@openclaw/normalization-core";
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import { readTrimmedStringAlias } from "../utils/string-readers.js";
@@ -50,11 +51,7 @@ function parseJsonObject(text: string): Record<string, unknown> | undefined {
if (!trimmed.startsWith("{")) {
return undefined;
}
try {
return asOptionalRecord(JSON.parse(trimmed));
} catch {
return undefined;
}
return asOptionalRecord(safeParseJson(trimmed));
}
function readStructuredToolPayload(content: unknown): Record<string, unknown> | undefined {
+2 -13
View File
@@ -1,4 +1,5 @@
// Renders chat canvas payloads into text and metadata for transcript output.
import { safeParseJson } from "@openclaw/normalization-core";
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import { parseFenceSpans } from "../../packages/markdown-core/src/fences.js";
@@ -19,18 +20,6 @@ type CanvasPreview = {
style?: string;
};
function tryParseJsonRecord(value: string | undefined): Record<string, unknown> | undefined {
if (typeof value !== "string") {
return undefined;
}
try {
const parsed = JSON.parse(value);
return asOptionalRecord(parsed);
} catch {
return undefined;
}
}
function getRecordStringField(
record: Record<string, unknown> | undefined,
key: string,
@@ -184,7 +173,7 @@ export function extractCanvasFromText(
outputText: string | undefined,
_toolName?: string,
): CanvasPreview | undefined {
const parsed = tryParseJsonRecord(outputText);
const parsed = outputText ? asOptionalRecord(safeParseJson(outputText)) : undefined;
return coerceCanvasPreview(parsed);
}