You need to parse arbitrary JSON into a typed, manipulable structure. A JSON file could contain numbers, strings, arrays, objects—all nested. Without an AST schema, you work with any and lose type safety. You need a schema that validates JSON structure and gives you a fully-typed tree for traversal and transformation.
import { Schema, Effect } from "effect"
// ============================================
// 1. JSON AST types
// ============================================
type JsonValue =
| { type: "null" }
| { type: "boolean"; value: boolean }
| { type: "number"; value: number }
| { type: "string"; value: string }
| { type: "array"; elements: JsonValue[] }
| { type: "object"; properties: Record<string, JsonValue> }
const JsonValue: Schema.Schema<JsonValue> = Schema.suspend(() =>
Schema.Union(
Schema.Struct({ type: Schema.Literal("null") }),
Schema.Struct({
type: Schema.Literal("boolean"),
value: Schema.Boolean,
}),
Schema.Struct({
type: Schema.Literal("number"),
value: Schema.Number,
}),
Schema.Struct({
type: Schema.Literal("string"),
value: Schema.String,
}),
Schema.Struct({
type: Schema.Literal("array"),
elements: Schema.Array(JsonValue),
}),
Schema.Struct({
type: Schema.Literal("object"),
properties: Schema.Record(Schema.String, JsonValue),
})
)
)
// ============================================
// 2. Convert raw JSON to AST
// ============================================
const jsonToAst = (json: unknown): JsonValue => {
if (json === null) {
return { type: "null" }
}
if (typeof json === "boolean") {
return { type: "boolean", value: json }
}
if (typeof json === "number") {
return { type: "number", value: json }
}
if (typeof json === "string") {
return { type: "string", value: json }
}
if (Array.isArray(json)) {
return {
type: "array",
elements: json.map(jsonToAst),
}
}
if (typeof json === "object") {
const properties: Record<string, JsonValue> = {}
for (const [key, value] of Object.entries(json)) {
properties[key] = jsonToAst(value)
}
return { type: "object", properties }
}
throw new Error(`Unknown JSON type: ${typeof json}`)
}
// ============================================
// 3. Convert AST back to JSON
// ============================================
const astToJson = (ast: JsonValue): unknown => {
switch (ast.type) {
case "null":
return null
case "boolean":
return ast.value
case "number":
return ast.value
case "string":
return ast.value
case "array":
return ast.elements.map(astToJson)
case "object":
const obj: Record<string, unknown> = {}
for (const [key, value] of Object.entries(ast.properties)) {
obj[key] = astToJson(value)
}
return obj
}
}
// ============================================
// 4. AST utilities
// ============================================
const findPaths = (
ast: JsonValue,
predicate: (v: JsonValue) => boolean,
path: string = "root"
): string[] => {
const results: string[] = []
if (predicate(ast)) {
results.push(path)
}
if (ast.type === "array") {
for (let i = 0; i < ast.elements.length; i++) {
results.push(
...findPaths(ast.elements[i], predicate, `${path}[${i}]`)
)
}
} else if (ast.type === "object") {
for (const [key, value] of Object.entries(ast.properties)) {
results.push(...findPaths(value, predicate, `${path}.${key}`))
}
}
return results
}
const transformAst = (
ast: JsonValue,
fn: (v: JsonValue) => JsonValue
): JsonValue => {
const transformed = fn(ast)
switch (transformed.type) {
case "array":
return {
...transformed,
elements: transformed.elements.map((e) => transformAst(e, fn)),
}
case "object":
const newProps: Record<string, JsonValue> = {}
for (const [key, value] of Object.entries(
transformed.properties
)) {
newProps[key] = transformAst(value, fn)
}
return { ...transformed, properties: newProps }
default:
return transformed
}
}
const prettyPrint = (
ast: JsonValue,
indent: number = 0
): Effect.Effect<void> =>
Effect.gen(function* () {
const prefix = " ".repeat(indent)
switch (ast.type) {
case "null":
yield* Effect.log(`${prefix}null`)
break
case "boolean":
yield* Effect.log(`${prefix}${ast.value}`)
break
case "number":
yield* Effect.log(`${prefix}${ast.value}`)
break
case "string":
yield* Effect.log(`${prefix}"${ast.value}"`)
break
case "array":
yield* Effect.log(`${prefix}[`)
for (let i = 0; i < ast.elements.length; i++) {
yield* prettyPrint(ast.elements[i], indent + 1)
if (i < ast.elements.length - 1) {
yield* Effect.log(",")
}
}
yield* Effect.log(`${prefix}]`)
break
case "object":
yield* Effect.log(`${prefix}{`)
const entries = Object.entries(ast.properties)
for (let i = 0; i < entries.length; i++) {
const [key, value] = entries[i]
yield* Effect.log(`${prefix} "${key}":`)
yield* prettyPrint(value, indent + 2)
if (i < entries.length - 1) {
yield* Effect.log(",")
}
}
yield* Effect.log(`${prefix}}`)
break
}
})
// ============================================
// 5. Application logic
// ============================================
const appLogic = Effect.gen(function* () {
console.log("=== JSON to AST Conversion ===\n")
const jsonData = {
name: "Effect-TS",
version: "3.0.0",
active: true,
tags: ["typescript", "functional", "effect"],
maintainers: [
{ name: "John Doe", email: "john@example.com" },
{ name: "Jane Smith", email: "jane@example.com" },
],
stats: {
downloads: 100000,
stars: 5000,
issues: 42,
},
deprecated: null,
}
console.log("Raw JSON:")
console.log(JSON.stringify(jsonData, null, 2))
console.log("\n--- Converting to AST ---\n")
const ast = jsonToAst(jsonData)
console.log("AST representation (excerpt):")
console.log(`Root type: ${ast.type}`)
if (ast.type === "object") {
console.log(`Properties: ${Object.keys(ast.properties).join(", ")}`)
}
console.log("\n--- Pretty Printed AST ---\n")
yield* prettyPrint(ast)
console.log("\n--- Finding string values ---\n")
const stringPaths = findPaths(
ast,
(v) => v.type === "string",
"root"
)
console.log("Paths with strings:")
for (const path of stringPaths) {
console.log(` ${path}`)
}
console.log("\n--- Transforming AST ---\n")
// Double all numbers
const transformed = transformAst(ast, (v) => {
if (v.type === "number") {
return { type: "number", value: v.value * 2 }
}
return v
})
console.log("After doubling all numbers:")
const result = astToJson(transformed)
if (typeof result === "object" && result !== null) {
const stats = (result as any).stats
console.log(` downloads: ${stats.downloads}`)
console.log(` stars: ${stats.stars}`)
}
console.log("\n--- Validation ---\n")
// Validate AST structure
const validationResult = yield* Effect.tryPromise({
try: () => Schema.decodeUnknown(JsonValue)(ast),
catch: (error) => {
const msg = error instanceof Error ? error.message : String(error)
return new Error(`AST validation failed: ${msg}`)
},
})
console.log("✅ AST is valid")
console.log(`Roundtrip successful: ${JSON.stringify(astToJson(ast)) === JSON.stringify(jsonData)}`)
return { ast, transformed }
})
// Run application
Effect.runPromise(appLogic)
.then(() => console.log("\n✅ AST parsing complete"))
.catch((error) => console.error(`Error: ${error.message}`))