import java.math.BigDecimal; import java.util.*; /** Small dependency-free JSON codec for API/configuration objects, not application data. */ final class Json { static Object parse(String source) { Parser parser = new Parser(source); Object result = parser.value(); parser.space(); if (parser.pos != source.length()) throw parser.invalid(); return result; } @SuppressWarnings("unchecked") static Map object(Object value) { if (!(value instanceof Map)) throw new IllegalArgumentException("Expected a JSON object"); return (Map) value; } static String write(Object value) { if (value == null) return "null"; if (value instanceof String text) { StringBuilder out = new StringBuilder("\""); for (char c : text.toCharArray()) { switch (c) { case '"' -> out.append("\\\""); case '\\' -> out.append("\\\\"); case '\n' -> out.append("\\n"); case '\r' -> out.append("\\r"); case '\t' -> out.append("\\t"); default -> { if (c < 32) out.append(String.format("\\u%04x", (int) c)); else out.append(c); } } } return out.append('"').toString(); } if (value instanceof Boolean || value instanceof BigDecimal || value instanceof Integer || value instanceof Long) return value.toString(); if (value instanceof Map map) { StringJoiner out = new StringJoiner(",", "{", "}"); map.forEach((k, v) -> out.add(write(Objects.toString(k)) + ":" + write(v))); return out.toString(); } if (value instanceof List list) { StringJoiner out = new StringJoiner(",", "[", "]"); list.forEach(v -> out.add(write(v))); return out.toString(); } throw new IllegalArgumentException("Unsupported JSON value"); } private static final class Parser { final String source; int pos; Parser(String source) { this.source = source; } IllegalArgumentException invalid() { return new IllegalArgumentException("Invalid JSON at offset " + pos); } void space() { while (pos < source.length() && " \r\n\t".indexOf(source.charAt(pos)) >= 0) pos++; } boolean take(char c) { space(); if (pos < source.length() && source.charAt(pos) == c) { pos++; return true; } return false; } Object value() { space(); if (pos >= source.length()) throw invalid(); char c = source.charAt(pos); if (c == '"') return string(); if (take('{')) { Map out = new LinkedHashMap<>(); if (take('}')) return out; do { space(); if (pos >= source.length() || source.charAt(pos) != '"') throw invalid(); String key = string(); if (!take(':') || out.containsKey(key)) throw invalid(); out.put(key, value()); } while (take(',')); if (!take('}')) throw invalid(); return out; } if (take('[')) { List out = new ArrayList<>(); if (take(']')) return out; do { out.add(value()); } while (take(',')); if (!take(']')) throw invalid(); return out; } for (String token : List.of("true", "false", "null")) { if (source.startsWith(token, pos)) { pos += token.length(); return token.equals("null") ? null : Boolean.valueOf(token); } } int start = pos; while (pos < source.length() && "-+0123456789.eE".indexOf(source.charAt(pos)) >= 0) pos++; String number = source.substring(start, pos); if (!number.matches("-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?")) throw invalid(); return new BigDecimal(number); } String string() { pos++; StringBuilder out = new StringBuilder(); while (pos < source.length()) { char c = source.charAt(pos++); if (c == '"') return out.toString(); if (c < 32) throw invalid(); if (c != '\\') { out.append(c); continue; } if (pos >= source.length()) throw invalid(); char escaped = source.charAt(pos++); switch (escaped) { case '"', '\\', '/' -> out.append(escaped); case 'b' -> out.append('\b'); case 'f' -> out.append('\f'); case 'n' -> out.append('\n'); case 'r' -> out.append('\r'); case 't' -> out.append('\t'); case 'u' -> { if (pos + 4 > source.length()) throw invalid(); String digits = source.substring(pos, pos + 4); if (!digits.matches("[0-9a-fA-F]{4}")) throw invalid(); out.append((char) Integer.parseInt(digits, 16)); pos += 4; } default -> throw invalid(); } } throw invalid(); } } }