百行代码实现JSON字符串解析工具类(KOTLIN版本)

原文: JFinal使用技巧-百行代码实现JSON字符串解析工具类(无三方依赖)

playground: https://pl.kotl.in/Xr8WAMzRV

import java.math.BigDecimal

object JsonParser {
  private const val MAX_DEPTH = 512

  @Suppress("UNCHECKED_CAST")
  fun parseMap(jsonString: String): Map<String, Any?> {
    return parse(jsonString) as Map<String, Any?>
  }

  @Suppress("UNCHECKED_CAST")
  fun parseList(jsonString: String): List<Any?> {
    return parse(jsonString) as List<Any?>
  }

  fun parse(jsonString: String): Any? {
    return Parser(jsonString.trim()).parseValue(0)
  }

  private class Parser(private val jsonStr: String) {
    private var index: Int = 0

    fun parseValue(depth: Int): Any? {
      if (depth > MAX_DEPTH) throw JsonException("Exceeded maximum nesting depth", index)
      skipWhitespace()
      if (index >= jsonStr.length) throw JsonException("Unexpected end of JSON", index)

      return when (val current = jsonStr[index]) {
        '{' -> parseObject(depth + 1)
        '[' -> parseArray(depth + 1)
        '"' -> parseString()
        in '0'..'9',
        '-' -> parseNumber()
        't',
        'f' -> parseBoolean()
        'n' -> parseNull()
        else -> throw JsonException("Unexpected character '$current'", index)
      }
    }

    private fun skipWhitespace() {
      while (index < jsonStr.length && jsonStr[index].isJsonWhitespace()) {
        index++
      }
    }

    private fun parseString(): String {
      val startIndex = ++index // Skip opening quote
      val sb = StringBuilder()
      while (index < jsonStr.length) {
        when (val c = jsonStr[index++]) {
          '"' -> return sb.toString()
          '\\' -> handleEscapeSequence(sb)
          else -> {
            if (c.code < 0x20) {
              throw JsonException(
                "Unescaped control character: 0x${c.code.toString(16)}",
                index - 1,
              )
            }
            sb.append(c)
          }
        }
      }
      throw JsonException("Unterminated string starting at $startIndex", startIndex)
    }

    private fun handleEscapeSequence(sb: StringBuilder) {
      if (index >= jsonStr.length) throw JsonException("Unterminated escape sequence", index - 1)
      when (val esc = jsonStr[index++]) {
        '"',
        '\\',
        '/' -> sb.append(esc)
        'b' -> sb.append('\b')
        'f' -> sb.append('\u000C')
        'n' -> sb.append('\n')
        'r' -> sb.append('\r')
        't' -> sb.append('\t')
        'u' -> parseUnicodeEscape(sb)
        else -> throw JsonException("Invalid escape character '$esc'", index - 1)
      }
    }

    private fun parseUnicodeEscape(sb: StringBuilder) {
      if (index + 4 > jsonStr.length) throw JsonException("Incomplete unicode escape", index - 1)
      val hex = jsonStr.substring(index, index + 4)
      index += 4
      try {
        val codePoint = hex.toInt(16)
        // Handle surrogate pairs
        if (codePoint in 0xD800..0xDBFF) { // High surrogate
          if (index + 6 > jsonStr.length || jsonStr[index] != '\\' || jsonStr[index + 1] != 'u') {
            throw JsonException("Missing low surrogate", index - 4)
          }
          index += 2 // Skip \u
          val lowHex = jsonStr.substring(index, index + 4)
          index += 4
          val lowCode = lowHex.toInt(16)
          if (lowCode !in 0xDC00..0xDFFF) {
            throw JsonException("Invalid low surrogate: \\u$lowHex", index - 4)
          }
          val fullCode = 0x10000 + (codePoint - 0xD800) * 0x400 + (lowCode - 0xDC00)
          sb.append(Character.toChars(fullCode))
        } else {
          sb.append(Character.toChars(codePoint))
        }
      } catch (e: NumberFormatException) {
        throw JsonException("Invalid unicode escape '\\u$hex'", index - 4)
      }
    }

    private fun parseObject(depth: Int): Map<String, Any?> {
      val map = LinkedHashMap<String, Any?>()
      index++ // Skip '{'
      skipWhitespace()

      if (consumeIf('}')) {
        return map
      }

      while (true) {
        skipWhitespace()
        val key = parseString()
        if (map.containsKey(key)) throw JsonException("Duplicate key '$key'", index)

        skipWhitespace()
        if (!consumeIf(':')) {
          throw JsonException("Expected ':' after key", index)
        }

        val value = parseValue(depth)
        map[key] = value
        skipWhitespace()

        if (consumeIf('}')) {
          return map
        }
        if (!consumeIf(',')) {
          throw JsonException("Expected ',' or '}'", index)
        }
        skipWhitespace()
        if (jsonStr.getOrNull(index) == '}') {
          throw JsonException("Trailing comma in object", index)
        }
      }
    }

    private fun parseArray(depth: Int): List<Any?> {
      val list = ArrayList<Any?>()
      index++ // Skip '['
      skipWhitespace()

      if (consumeIf(']')) {
        return list
      }

      while (true) {
        skipWhitespace()
        list.add(parseValue(depth))
        skipWhitespace()

        if (consumeIf(']')) {
          return list
        }
        if (!consumeIf(',')) {
          throw JsonException("Expected ',' or ']'", index)
        }
        skipWhitespace()
        if (jsonStr.getOrNull(index) == ']') {
          throw JsonException("Trailing comma in array", index)
        }
      }
    }

    private fun parseNumber(): Number {
      val start = index
      var hasExponent = false
      var hasDecimal = false

      if (jsonStr[index] == '-') index++

      // Check leading zero
      if (index < jsonStr.length && jsonStr[index] == '0') {
        index++
        if (index < jsonStr.length && jsonStr[index] in '0'..'9') {
          throw JsonException("Leading zeros not allowed", index)
        }
      } else {
        while (index < jsonStr.length && jsonStr[index] in '0'..'9') {
          index++
        }
      }

      // Decimal part
      if (index < jsonStr.length && jsonStr[index] == '.') {
        hasDecimal = true
        index++
        if (index >= jsonStr.length || jsonStr[index] !in '0'..'9') {
          throw JsonException("Expected digit after decimal point", index)
        }
        while (index < jsonStr.length && jsonStr[index] in '0'..'9') {
          index++
        }
      }

      // Exponent part
      if (index < jsonStr.length && jsonStr[index] in "eE") {
        hasExponent = true
        index++
        if (index < jsonStr.length && jsonStr[index] in "+-") {
          index++
        }
        if (index >= jsonStr.length || jsonStr[index] !in '0'..'9') {
          throw JsonException("Expected digit in exponent", index)
        }
        while (index < jsonStr.length && jsonStr[index] in '0'..'9') {
          index++
        }
      }

      val numStr = jsonStr.substring(start, index)
      return try {
        when {
          hasDecimal || hasExponent -> numStr.toDouble()
          numStr.toLongOrNull() != null -> {
            val longVal = numStr.toLong()
            if (longVal >= Int.MIN_VALUE && longVal <= Int.MAX_VALUE) longVal.toInt() else longVal
          }
          else -> BigDecimal(numStr)
        }
      } catch (e: NumberFormatException) {
        throw JsonException("Invalid number format: '$numStr'", start)
      }
    }

    private fun parseBoolean(): Boolean {
      return when {
        jsonStr.startsWith("true", index) -> {
          index += 4
          true
        }
        jsonStr.startsWith("false", index) -> {
          index += 5
          false
        }
        else -> throw JsonException("Expected boolean value", index)
      }
    }

    private fun parseNull(): Any? {
      if (jsonStr.startsWith("null", index)) {
        index += 4
        return null
      }
      throw JsonException("Expected null value", index)
    }

    private fun Char.isJsonWhitespace() = this in " \t\n\r"

    private fun consumeIf(expected: Char): Boolean {
      if (index < jsonStr.length && jsonStr[index] == expected) {
        index++
        return true
      }
      return false
    }
  }

  class JsonException(message: String, position: Int) :
    RuntimeException("$message at position $position")
}

/*
 * ===== JSON Parser Test =====
 * ✅ PASS: Empty object
 * ✅ PASS: Simple object
 * ✅ PASS: Nested structures
 * ✅ PASS: Empty array
 * ✅ PASS: Mixed type array
 * ✅ PASS: Unicode escape
 * ✅ PASS: Scientific notation
 *
 * ===== Error Handling Tests =====
 * ✅ PASS: Invalid token - Unexpected character 'u' at position 8
 * ✅ PASS: Trailing comma - Unexpected character ',' at position 3
 * ✅ PASS: Missing comma - Expected ',' or '}' at position 7
 * ✅ PASS: Duplicate key - Duplicate key 'dup' at position 15
 * ✅ PASS: Unclosed array - Unexpected end of JSON at position 6
 * ✅ PASS: Leading zero - Leading zeros not allowed at position 9
 * ✅ PASS: Control character - Unescaped control character: 0x0 at position 17
 * ✅ PASS: Extra bracket - Trailing comma in array at position 7
 * ✅ PASS: Excess nesting - Exceeded maximum nesting depth at position 522
 */
fun main() {
  val parser = JsonParser
  println("===== JSON Parser Test =====")

  // Test 1: 空对象
  testParseMap(parser, "{}", emptyMap(), "Empty object")

  // Test 2: 简单对象
  testParseMap(
    parser,
    """{"name":"Alice","age":25,"active":true}""",
    mapOf("name" to "Alice", "age" to 25, "active" to true),
    "Simple object",
  )

  // Test 3: 嵌套对象
  testParseMap(
    parser,
    """{"user":{"id":1,"prefs":{"theme":"dark"}},"tags":["kotlin","json"]}""",
    mapOf(
      "user" to mapOf("id" to 1, "prefs" to mapOf("theme" to "dark")),
      "tags" to listOf("kotlin", "json"),
    ),
    "Nested structures",
  )

  // Test 4: 空数组
  testParseList(parser, "[]", emptyList(), "Empty array")

  // Test 5: 混合类型数组
  testParseList(
    parser,
    """[null, 42, -3.14, "text", true, false]""",
    listOf(null, 42, -3.14, "text", true, false),
    "Mixed type array",
  )

  // Test 6: Unicode 转义
  testParseString(parser, """"Hello\u0020World!\uD83D\uDE00"""", "Hello World!😀", "Unicode escape")

  // Test 7: 科学计数法数字
  testParseNumber(parser, "1.23e4", 12300.0, "Scientific notation")

  // Test 8: 错误处理
  testErrorHandling(parser)
}

private fun testParseMap(
  parser: JsonParser,
  json: String,
  expected: Map<String, Any?>,
  name: String,
) {
  try {
    val result = parser.parseMap(json)
    if (result == expected) {
      println("✅ PASS: $name")
    } else {
      println("❌ FAIL: $name\n  Expected: $expected\n  Got:      $result")
    }
  } catch (e: Exception) {
    println("❌ ERROR: $name - ${e.message}")
  }
}

private fun testParseList(parser: JsonParser, json: String, expected: List<Any?>, name: String) {
  try {
    val result = parser.parseList(json)
    if (result == expected) {
      println("✅ PASS: $name")
    } else {
      println("❌ FAIL: $name\n  Expected: $expected\n  Got:      $result")
    }
  } catch (e: Exception) {
    println("❌ ERROR: $name - ${e.message}")
  }
}

private fun testParseString(parser: JsonParser, json: String, expected: String, name: String) {
  try {
    val result = parser.parse(json) as String
    if (result == expected) {
      println("✅ PASS: $name")
    } else {
      println("❌ FAIL: $name\n  Expected: '$expected'\n  Got:      '$result'")
    }
  } catch (e: Exception) {
    println("❌ ERROR: $name - ${e.message}")
  }
}

private fun testParseNumber(parser: JsonParser, json: String, expected: Number, name: String) {
  try {
    val result = parser.parse(json) as Number
    if (result.toDouble() == expected.toDouble()) {
      println("✅ PASS: $name")
    } else {
      println("❌ FAIL: $name\n  Expected: $expected\n  Got:      $result")
    }
  } catch (e: Exception) {
    println("❌ ERROR: $name - ${e.message}")
  }
}

private fun testErrorHandling(parser: JsonParser) {
  // 创建超过512层嵌套的JSON
  val deepNested = buildString {
    append("{\"nested\":")
    repeat(600) { append('[') }
    append("123")
    repeat(600) { append(']') }
    append('}')
  }

  // 创建包含未转义控制字符的JSON
  val controlCharJson = buildString {
    append("{\"control\": \"text")
    append(0.toChar()) // 添加未转义的控制字符 (U+0000)
    append("\"}")
  }

  val tests =
    listOf(
      """{"key": undefined}""" to "Invalid token",
      """[1,,2]""" to "Trailing comma",
      """{"a":1 "b":2}""" to "Missing comma",
      """{"dup":1, "dup":2}""" to "Duplicate key",
      """[1,2,3""" to "Unclosed array", // 修正为未闭合数组
      """{"num": 012}""" to "Leading zero",
      controlCharJson to "Control character", // 使用包含实际控制字符的JSON
      """[1,2,3,]]""" to "Extra bracket",
      deepNested to "Excess nesting", // 使用深层嵌套的JSON
    )

  println("\n===== Error Handling Tests =====")
  tests.forEach { (json, testName) ->
    try {
      parser.parse(json)
      println("❌ FAIL: $testName - Expected error but parsed successfully")
    } catch (e: JsonParser.JsonException) {
      println("✅ PASS: $testName - ${e.message}")
    } catch (e: Exception) {
      println("❌ ERROR: $testName - Wrong exception: ${e.javaClass.simpleName}")
    }
  }
}


评论区

热门分享

扫码入社