// JSON Serialization and Deserialization for Lux // // Provides type-safe JSON encoding and decoding with support // for schema versioning and custom codecs. // // Usage: // let json = Json.encode(user) // Serialize to JSON string // let user = Json.decode(json) // Deserialize from JSON string // ============================================================ // JSON Value Type // ============================================================ // Represents any JSON value type JsonValue = | JsonNull | JsonBool(Bool) | JsonInt(Int) | JsonFloat(Float) | JsonString(String) | JsonArray(List) | JsonObject(List<(String, JsonValue)>) // ============================================================ // Encoding Primitives // ============================================================ // Escape a string for JSON pub fn escapeString(s: String): String = { let escaped = String.replace(s, "\\", "\\\\") let escaped = String.replace(escaped, "\"", "\\\"") let escaped = String.replace(escaped, "\n", "\\n") let escaped = String.replace(escaped, "\r", "\\r") let escaped = String.replace(escaped, "\t", "\\t") escaped } // Encode a JsonValue to a JSON string pub fn encode(value: JsonValue): String = { match value { JsonNull => "null", JsonBool(b) => if b then "true" else "false", JsonInt(n) => toString(n), JsonFloat(f) => toString(f), JsonString(s) => "\"" + escapeString(s) + "\"", JsonArray(items) => { let encodedItems = List.map(items, encode) "[" + String.join(encodedItems, ",") + "]" }, JsonObject(fields) => { let encodedFields = List.map(fields, fn(field: (String, JsonValue)): String => { match field { (key, val) => "\"" + escapeString(key) + "\":" + encode(val) } }) "{" + String.join(encodedFields, ",") + "}" } } } // Pretty-print a JsonValue with indentation pub fn encodePretty(value: JsonValue): String = encodePrettyIndent(value, 0) fn encodePrettyIndent(value: JsonValue, indent: Int): String = { let spaces = String.repeat(" ", indent) let nextSpaces = String.repeat(" ", indent + 1) match value { JsonNull => "null", JsonBool(b) => if b then "true" else "false", JsonInt(n) => toString(n), JsonFloat(f) => toString(f), JsonString(s) => "\"" + escapeString(s) + "\"", JsonArray(items) => { if List.length(items) == 0 then "[]" else { let encodedItems = List.map(items, fn(item: JsonValue): String => nextSpaces + encodePrettyIndent(item, indent + 1) ) "[\n" + String.join(encodedItems, ",\n") + "\n" + spaces + "]" } }, JsonObject(fields) => { if List.length(fields) == 0 then "{}" else { let encodedFields = List.map(fields, fn(field: (String, JsonValue)): String => { match field { (key, val) => nextSpaces + "\"" + escapeString(key) + "\": " + encodePrettyIndent(val, indent + 1) } }) "{\n" + String.join(encodedFields, ",\n") + "\n" + spaces + "}" } } } } // ============================================================ // Type-specific Encoders // ============================================================ // Encode primitives pub fn encodeNull(): JsonValue = JsonNull pub fn encodeBool(b: Bool): JsonValue = JsonBool(b) pub fn encodeInt(n: Int): JsonValue = JsonInt(n) pub fn encodeFloat(f: Float): JsonValue = JsonFloat(f) pub fn encodeString(s: String): JsonValue = JsonString(s) // Encode a list pub fn encodeList(items: List, encodeItem: fn(A): JsonValue): JsonValue = JsonArray(List.map(items, encodeItem)) // Encode an optional value pub fn encodeOption(opt: Option, encodeItem: fn(A): JsonValue): JsonValue = match opt { None => JsonNull, Some(value) => encodeItem(value) } // Encode a Result pub fn encodeResult( result: Result, encodeOk: fn(T): JsonValue, encodeErr: fn(E): JsonValue ): JsonValue = match result { Ok(value) => JsonObject([ ("ok", encodeOk(value)) ]), Err(error) => JsonObject([ ("error", encodeErr(error)) ]) } // ============================================================ // Object Building Helpers // ============================================================ // Create an empty JSON object pub fn object(): JsonValue = JsonObject([]) // Add a field to a JSON object pub fn withField(obj: JsonValue, key: String, value: JsonValue): JsonValue = match obj { JsonObject(fields) => JsonObject(List.concat(fields, [(key, value)])), _ => obj // Not an object, return unchanged } // Add a string field pub fn withString(obj: JsonValue, key: String, value: String): JsonValue = withField(obj, key, JsonString(value)) // Add an int field pub fn withInt(obj: JsonValue, key: String, value: Int): JsonValue = withField(obj, key, JsonInt(value)) // Add a bool field pub fn withBool(obj: JsonValue, key: String, value: Bool): JsonValue = withField(obj, key, JsonBool(value)) // Add an optional field (only adds if Some) pub fn withOptional(obj: JsonValue, key: String, opt: Option, encodeItem: fn(A): JsonValue): JsonValue = match opt { None => obj, Some(value) => withField(obj, key, encodeItem(value)) } // ============================================================ // Decoding // ============================================================ // Result type for parsing type ParseResult = Result // Get a field from a JSON object pub fn getField(obj: JsonValue, key: String): Option = match obj { JsonObject(fields) => findField(fields, key), _ => None } fn findField(fields: List<(String, JsonValue)>, key: String): Option = match List.head(fields) { None => None, Some(field) => match field { (k, v) => if k == key then Some(v) else findField(Option.getOrElse(List.tail(fields), []), key) } } // Get a string field pub fn getString(obj: JsonValue, key: String): Option = match getField(obj, key) { Some(JsonString(s)) => Some(s), _ => None } // Get an int field pub fn getInt(obj: JsonValue, key: String): Option = match getField(obj, key) { Some(JsonInt(n)) => Some(n), _ => None } // Get a bool field pub fn getBool(obj: JsonValue, key: String): Option = match getField(obj, key) { Some(JsonBool(b)) => Some(b), _ => None } // Get an array field pub fn getArray(obj: JsonValue, key: String): Option> = match getField(obj, key) { Some(JsonArray(items)) => Some(items), _ => None } // Get an object field pub fn getObject(obj: JsonValue, key: String): Option = match getField(obj, key) { Some(JsonObject(_) as obj) => Some(obj), _ => None } // ============================================================ // Simple JSON Parser // ============================================================ // Parse a JSON string into a JsonValue // Note: This is a simplified parser for common cases pub fn parse(json: String): Result = parseValue(String.trim(json), 0).mapResult(fn(r: (JsonValue, Int)): JsonValue => { match r { (value, _) => value } }) fn parseValue(json: String, pos: Int): Result<(JsonValue, Int), String> = { let c = String.charAt(json, pos) match c { "n" => parseNull(json, pos), "t" => parseTrue(json, pos), "f" => parseFalse(json, pos), "\"" => parseString(json, pos), "[" => parseArray(json, pos), "{" => parseObject(json, pos), "-" => parseNumber(json, pos), _ => if isDigit(c) then parseNumber(json, pos) else if c == " " || c == "\n" || c == "\r" || c == "\t" then parseValue(json, pos + 1) else Err("Unexpected character at position " + toString(pos)) } } fn parseNull(json: String, pos: Int): Result<(JsonValue, Int), String> = if String.substring(json, pos, pos + 4) == "null" then Ok((JsonNull, pos + 4)) else Err("Expected 'null' at position " + toString(pos)) fn parseTrue(json: String, pos: Int): Result<(JsonValue, Int), String> = if String.substring(json, pos, pos + 4) == "true" then Ok((JsonBool(true), pos + 4)) else Err("Expected 'true' at position " + toString(pos)) fn parseFalse(json: String, pos: Int): Result<(JsonValue, Int), String> = if String.substring(json, pos, pos + 5) == "false" then Ok((JsonBool(false), pos + 5)) else Err("Expected 'false' at position " + toString(pos)) fn parseString(json: String, pos: Int): Result<(JsonValue, Int), String> = { // Skip opening quote let start = pos + 1 let result = parseStringContent(json, start, "") result.mapResult(fn(r: (String, Int)): (JsonValue, Int) => { match r { (s, endPos) => (JsonString(s), endPos) } }) } fn parseStringContent(json: String, pos: Int, acc: String): Result<(String, Int), String> = { let c = String.charAt(json, pos) if c == "\"" then Ok((acc, pos + 1)) else if c == "\\" then { let nextC = String.charAt(json, pos + 1) let escaped = match nextC { "n" => "\n", "r" => "\r", "t" => "\t", "\"" => "\"", "\\" => "\\", _ => nextC } parseStringContent(json, pos + 2, acc + escaped) } else if c == "" then Err("Unterminated string") else parseStringContent(json, pos + 1, acc + c) } fn parseNumber(json: String, pos: Int): Result<(JsonValue, Int), String> = { let result = parseNumberDigits(json, pos, "") result.mapResult(fn(r: (String, Int)): (JsonValue, Int) => { match r { (numStr, endPos) => { // Check if it's a float if String.contains(numStr, ".") then (JsonFloat(parseFloat(numStr)), endPos) else (JsonInt(parseInt(numStr)), endPos) } } }) } fn parseNumberDigits(json: String, pos: Int, acc: String): Result<(String, Int), String> = { let c = String.charAt(json, pos) if isDigit(c) || c == "." || c == "-" || c == "e" || c == "E" || c == "+" then parseNumberDigits(json, pos + 1, acc + c) else if acc == "" then Err("Expected number at position " + toString(pos)) else Ok((acc, pos)) } fn parseArray(json: String, pos: Int): Result<(JsonValue, Int), String> = { // Skip opening bracket and whitespace let startPos = skipWhitespace(json, pos + 1) if String.charAt(json, startPos) == "]" then Ok((JsonArray([]), startPos + 1)) else parseArrayItems(json, startPos, []) } fn parseArrayItems(json: String, pos: Int, acc: List): Result<(JsonValue, Int), String> = { match parseValue(json, pos) { Err(e) => Err(e), Ok((value, nextPos)) => { let newAcc = List.concat(acc, [value]) let afterWhitespace = skipWhitespace(json, nextPos) let c = String.charAt(json, afterWhitespace) if c == "]" then Ok((JsonArray(newAcc), afterWhitespace + 1)) else if c == "," then parseArrayItems(json, skipWhitespace(json, afterWhitespace + 1), newAcc) else Err("Expected ',' or ']' at position " + toString(afterWhitespace)) } } } fn parseObject(json: String, pos: Int): Result<(JsonValue, Int), String> = { // Skip opening brace and whitespace let startPos = skipWhitespace(json, pos + 1) if String.charAt(json, startPos) == "}" then Ok((JsonObject([]), startPos + 1)) else parseObjectFields(json, startPos, []) } fn parseObjectFields(json: String, pos: Int, acc: List<(String, JsonValue)>): Result<(JsonValue, Int), String> = { // Parse key match parseString(json, pos) { Err(e) => Err(e), Ok((keyValue, afterKey)) => { match keyValue { JsonString(key) => { let colonPos = skipWhitespace(json, afterKey) if String.charAt(json, colonPos) != ":" then Err("Expected ':' at position " + toString(colonPos)) else { let valuePos = skipWhitespace(json, colonPos + 1) match parseValue(json, valuePos) { Err(e) => Err(e), Ok((value, afterValue)) => { let newAcc = List.concat(acc, [(key, value)]) let afterWhitespace = skipWhitespace(json, afterValue) let c = String.charAt(json, afterWhitespace) if c == "}" then Ok((JsonObject(newAcc), afterWhitespace + 1)) else if c == "," then parseObjectFields(json, skipWhitespace(json, afterWhitespace + 1), newAcc) else Err("Expected ',' or '}' at position " + toString(afterWhitespace)) } } } }, _ => Err("Expected string key at position " + toString(pos)) } } } } fn skipWhitespace(json: String, pos: Int): Int = { let c = String.charAt(json, pos) if c == " " || c == "\n" || c == "\r" || c == "\t" then skipWhitespace(json, pos + 1) else pos } fn isDigit(c: String): Bool = c == "0" || c == "1" || c == "2" || c == "3" || c == "4" || c == "5" || c == "6" || c == "7" || c == "8" || c == "9" // ============================================================ // Codec Type (for automatic serialization) // ============================================================ // A codec can both encode and decode a type type Codec = { encode: fn(A): JsonValue, decode: fn(JsonValue): Result } // Create a codec from encode/decode functions pub fn codec( enc: fn(A): JsonValue, dec: fn(JsonValue): Result ): Codec = { encode: enc, decode: dec } // Built-in codecs pub fn stringCodec(): Codec = codec( encodeString, fn(json: JsonValue): Result => match json { JsonString(s) => Ok(s), _ => Err("Expected string") } ) pub fn intCodec(): Codec = codec( encodeInt, fn(json: JsonValue): Result => match json { JsonInt(n) => Ok(n), _ => Err("Expected int") } ) pub fn boolCodec(): Codec = codec( encodeBool, fn(json: JsonValue): Result => match json { JsonBool(b) => Ok(b), _ => Err("Expected bool") } ) pub fn listCodec(itemCodec: Codec): Codec> = codec( fn(items: List): JsonValue => encodeList(items, itemCodec.encode), fn(json: JsonValue): Result, String> => match json { JsonArray(items) => decodeAll(items, itemCodec.decode), _ => Err("Expected array") } ) fn decodeAll(items: List, decode: fn(JsonValue): Result): Result, String> = { match List.head(items) { None => Ok([]), Some(item) => match decode(item) { Err(e) => Err(e), Ok(decoded) => match decodeAll(Option.getOrElse(List.tail(items), []), decode) { Err(e) => Err(e), Ok(rest) => Ok(List.concat([decoded], rest)) } } } } pub fn optionCodec(itemCodec: Codec): Codec> = codec( fn(opt: Option): JsonValue => encodeOption(opt, itemCodec.encode), fn(json: JsonValue): Result, String> => match json { JsonNull => Ok(None), _ => itemCodec.decode(json).mapResult(fn(a: A): Option => Some(a)) } ) // ============================================================ // Helper for Result.mapResult // ============================================================ fn mapResult(result: Result, f: fn(A): B): Result = match result { Ok(a) => Ok(f(a)), Err(e) => Err(e) }