JSON Schema Validator (java, written by Codex)
envgap__codex__java-t1-2
Written by a coding agent; not on GitHubWritten 2026-03-02
01 / FAILURE SIGNATURE
As the study recorded it
None
Not a benchmark task.
- The project already builds and runs before the fix, so there is nothing to repair.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
pom.xml- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/java-t1 #2 · read the task the agent was given
Codex wrote this java project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: JSON Schema Validator Write a program that validates JSON documents against user-defined schema rules. It should support type checking, required fields, nested object validation, array constraints, and pattern matching. It should collect all violations — not stop at the first — and produce a detailed validation report. FUNCTIONAL REQUIREMENTS: - Accept two command-line arguments: a JSON data file and a JSON schema file - Support validation rules: type checking (string, number, boolean, array, object, null), required fields, min/max for numbers, minLength/maxLength for strings, enum values, regex pattern matching - Validate nested objects and arrays recursively to any depth - Validate array items (all items must conform to a specified type or sub-schema) - Collect ALL validation errors across the entire document, reporting the full JSON path for each violation (e.g., $.users[2].email) - Print the validation report to the console with one line per violation showing path, expected rule, and actual value - Save the validation report as validation_report.json with structured error objects - If no input files are given, generate a sample schema and a sample data file with intentional violations across different rule types, then validate - Handle malformed JSON input gracefully with clear error messages distinguishing parse errors from validation errors Create a complete Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include: - Source code - pom.xml with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tmlr.codex_generated.p02</groupId>
<artifactId>json-schema-validator</artifactId>
<version>1.0.0</version>
<name>JSON Schema Validator</name>
<description>Recursive JSON schema validator with full violation reporting.</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<mainClass>JsonSchemaValidator</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
README.md
# JSON Schema Validator (Java) CLI validator that checks JSON documents against user-defined schema rules and reports every violation with full JSON paths. ## Features - Accepts `data.json` and `schema.json` command-line arguments. - Supports: - `type` (`string`, `number`, `boolean`, `array`, `object`, `null`) - `required` - `min` / `max` (`minimum` / `maximum` also supported) - `minLength` / `maxLength` - `enum` - `pattern` (regex) - `minItems` / `maxItems` - Recursive validation for nested objects and arrays. - Validates all array items using `items` schema. - Collects all errors with paths like `$.users[2].email`. - Prints one line per violation with path, expected rule, and actual value. - Writes `validation_report.json` with structured error objects. - If no inputs are given, generates `sample_data.json` and `sample_schema.json` with intentional violations. - Distinguishes parse errors from validation errors using an internal JSON parser. ## Requirements - Ubuntu 22.04 - JDK 17+ ## Dependencies (Pinned) No external runtime dependencies are used. - Direct runtime dependencies: none - Transitive runtime dependencies: none `pom.xml` includes pinned Maven plugin versions. ## Build and Run ### Option A: JDK only ```bash cd TMLR/code_generation/codex_generated/p_02/java mkdir -p out javac -d out src/main/java/JsonSchemaValidator.java java -cp out JsonSchemaValidator ./data.json ./schema.json ``` Run with generated sample files: ```bash java -cp out JsonSchemaValidator ``` ### Option B: Maven ```bash cd TMLR/code_generation/codex_generated/p_02/java mvn -q compile mvn -q exec:java -Dexec.args="./data.json ./schema.json" ``` Run with generated samples: ```bash mvn -q exec:java ``` ## Output - Console: one line per violation. - `validation_report.json`: structured validation report. - If no arguments are provided: - `sample_data.json` - `sample_schema.json`
src/main/java/JsonSchemaValidator.java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class JsonSchemaValidator {
private static final Object MISSING = new Object();
public static void main(String[] args) {
try {
int exit = run(args);
System.exit(exit);
} catch (Exception ex) {
System.err.println("Unexpected failure: " + ex.getMessage());
System.exit(1);
}
}
private static int run(String[] args) throws IOException {
if (args.length != 0 && args.length != 2) {
System.err.println("Usage: java -cp out JsonSchemaValidator <data.json> <schema.json>");
System.err.println(" or: java -cp out JsonSchemaValidator");
return 1;
}
Path dataPath;
Path schemaPath;
if (args.length == 0) {
dataPath = Paths.get("sample_data.json").toAbsolutePath().normalize();
schemaPath = Paths.get("sample_schema.json").toAbsolutePath().normalize();
writeJson(dataPath, generateSampleData());
writeJson(schemaPath, generateSampleSchema());
System.out.println("No input files provided. Generated sample data: " + dataPath);
System.out.println("No input files provided. Generated sample schema: " + schemaPath);
} else {
dataPath = Paths.get(args[0]).toAbsolutePath().normalize();
schemaPath = Paths.get(args[1]).toAbsolutePath().normalize();
}
Object data;
Object schema;
try {
data = loadJson(dataPath, "Data");
} catch (RuntimeException ex) {
System.err.println(ex.getMessage());
return 1;
}
try {
schema = loadJson(schemaPath, "Schema");
} catch (RuntimeException ex) {
System.err.println(ex.getMessage());
return 1;
}
List<ValidationError> errors = new ArrayList<>();
validate(data, schema, "$", errors);
printConsoleReport(errors);
Map<String, Object> report = new LinkedHashMap<>();
report.put("timestamp", Instant.now().toString());
report.put("dataFile", dataPath.toString());
report.put("schemaFile", schemaPath.toString());
report.put("valid", errors.isEmpty());
report.put("errorCount", errors.size());
List<Object> errorObjects = new ArrayList<>();
for (ValidationError error : errors) {
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("path", error.path);
entry.put("rule", error.rule);
entry.put("expected", error.expected);
entry.put("actual", error.actual);
errorObjects.add(entry);
}
report.put("errors", errorObjects);
Path reportPath = Paths.get("validation_report.json").toAbsolutePath().normalize();
writeJson(reportPath, report);
System.out.println("Saved structured report: " + reportPath);
return errors.isEmpty() ? 0 : 1;
}
private static void printConsoleReport(List<ValidationError> errors) {
if (errors.isEmpty()) {
System.out.println("Validation succeeded: no violations found.");
return;
}
System.out.println("Validation failed: " + errors.size() + " violation(s) found.");
for (ValidationError error : errors) {
System.out.println(
error.path + " | expected: " + error.rule + " -> " + error.expected + " | actual: " + error.actual
);
}
}
private static void validate(Object data, Object rawSchema, String path, List<ValidationError> errors) {
if (!(rawSchema instanceof Map<?, ?>)) {
return;
}
@SuppressWarnings("unchecked")
Map<String, Object> schema = (Map<String, Object>) rawSchema;
Object declaredType = schema.get("type");
if (declaredType != null) {
List<String> allowedTypes = new ArrayList<>();
if (declaredType instanceof String) {
allowedTypes.add((String) declaredType);
} else if (declaredType instanceof List<?>) {
for (Object item : (List<?>) declaredType) {
if (item instanceof String) {
allowedTypes.add((String) item);
}
}
}
if (!allowedTypes.isEmpty()) {
boolean matches = false;
for (String expectedType : allowedTypes) {
if (matchesType(data, expectedType)) {
matches = true;
break;
}
}
if (!matches) {
String expected = String.join(" | ", allowedTypes);
addError(errors, path, "type", expected, data);
return;
}
}
}
Object enumValue = schema.get("enum");
if (enumValue instanceof List<?>) {
boolean found = false;
for (Object candidate : (List<?>) enumValue) {
if (deepEquals(candidate, data)) {
found = true;
break;
}
}
if (!found) {
addError(errors, path, "enum", JsonWriter.toJson(enumValue), data);
}
}
String dataType = jsonTypeOf(data);
if ("number".equals(dataType)) {
double value = ((Number) data).doubleValue();
Number min = numberOrNull(schema.get("min"));
Number max = numberOrNull(schema.get("max"));
Number minimum = numberOrNull(schema.get("minimum"));
Number maximum = numberOrNull(schema.get("maximum"));
if (min != null && value < min.doubleValue()) {
addError(errors, path, "min", ">= " + trimNumber(min.doubleValue()), data);
}
if (max != null && value > max.doubleValue()) {
addError(errors, path, "max", "<= " + trimNumber(max.doubleValue()), data);
}
if (minimum != null && value < minimum.doubleValue()) {
addError(errors, path, "minimum", ">= " + trimNumber(minimum.doubleValue()), data);
}
if (maximum != null && value > maximum.doubleValue()) {
addError(errors, path, "maximum", "<= " + trimNumber(maximum.doubleValue()), data);
}
}
if ("string".equals(dataType)) {
String value = (String) data;
Number minLength = numberOrNull(schema.get("minLength"));
Number maxLength = numberOrNull(schema.get("maxLength"));
if (minLength != null && value.length() < minLength.intValue()) {
addError(errors, path, "minLength", "length >= " + minLength.intValue(), data);
}
if (maxLength != null && value.length() > maxLength.intValue()) {
addError(errors, path, "maxLength", "length <= " + maxLength.intValue(), data);
}
if (schema.get("pattern") instanceof String) {
String pattern = (String) schema.get("pattern");
try {
if (!Pattern.compile(pattern).matcher(value).find()) {
addError(errors, path, "pattern", "match /" + pattern + "/", data);
}
} catch (PatternSyntaxException ex) {
addError(errors, path, "pattern", "invalid regex \"" + pattern + "\": " + ex.getMessage(), data);
}
}
}
if ("object".equals(dataType)) {
@SuppressWarnings("unchecked")
Map<String, Object> objectData = (Map<String, Object>) data;
if (schema.get("required") instanceof List<?>) {
for (Object key : (List<?>) schema.get("required")) {
if (key instanceof String && !objectData.containsKey(key)) {
addError(errors, path + "." + key, "required", "field must exist", MISSING);
}
}
}
if (schema.get("properties") instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> properties = (Map<String, Object>) schema.get("properties");
for (Map.Entry<String, Object> entry : properties.entrySet()) {
if (objectData.containsKey(entry.getKey())) {
validate(objectData.get(entry.getKey()), entry.getValue(), path + "." + entry.getKey(), errors);
}
}
}
}
if ("array".equals(dataType)) {
@SuppressWarnings("unchecked")
List<Object> listData = (List<Object>) data;
Number minItems = numberOrNull(schema.get("minItems"));
Number maxItems = numberOrNull(schema.get("maxItems"));
if (minItems != null && listData.size() < minItems.intValue()) {
addError(errors, path, "minItems", "items >= " + minItems.intValue(), data);
}
if (maxItems != null && listData.size() > maxItems.intValue()) {
addError(errors, path, "maxItems", "items <= " + maxItems.intValue(), data);
}
if (schema.get("items") instanceof Map<?, ?>) {
Object itemSchema = schema.get("items");
for (int i = 0; i < listData.size(); i++) {
validate(listData.get(i), itemSchema, path + "[" + i + "]", errors);
}
}
}
}
private static void addError(List<ValidationError> errors, String path, String rule, String expected, Object actualRaw) {
errors.add(new ValidationError(path, rule, expected, valuePreview(actualRaw)));
}
private static boolean matchesType(Object value, String expectedType) {
return Objects.equals(jsonTypeOf(value), expectedType);
}
private static String jsonTypeOf(Object value) {
if (value == null) return "null";
if (value instanceof Boolean) return "boolean";
if (value instanceof String) return "string";
if (value instanceof Number) return "number";
if (value instanceof List<?>) return "array";
if (value instanceof Map<?, ?>) return "object";
return value.getClass().getSimpleName().toLowerCase(Locale.ROOT);
}
private static boolean deepEquals(Object left, Object right) {
if (left == right) return true;
if (left == null || right == null) return false;
if (left instanceof Number && right instanceof Number) {
return Double.compare(((Number) left).doubleValue(), ((Number) right).doubleValue()) == 0;
}
if (left instanceof String && right instanceof String) {
return left.equals(right);
}
if (left instanceof Boolean && right instanceof Boolean) {
return left.equals(right);
}
if (left instanceof List<?> && right instanceof List<?>) {
List<?> a = (List<?>) left;
List<?> b = (List<?>) right;
if (a.size() != b.size()) return false;
for (int i = 0; i < a.size(); i++) {
if (!deepEquals(a.get(i), b.get(i))) return false;
}
return true;
}
if (left instanceof Map<?, ?> && right instanceof Map<?, ?>) {
Map<?, ?> a = (Map<?, ?>) left;
Map<?, ?> b = (Map<?, ?>) right;
if (a.size() != b.size()) return false;
for (Map.Entry<?, ?> entry : a.entrySet()) {
if (!b.containsKey(entry.getKey())) return false;
if (!deepEquals(entry.getValue(), b.get(entry.getKey()))) return false;
}
return true;
}
return false;
}
private static Number numberOrNull(Object value) {
return value instanceof Number ? (Number) value : null;
}
private static String valuePreview(Object value) {
if (value == MISSING) return "undefined";
if (value == null) return "null";
if (value instanceof String || value instanceof Number || value instanceof Boolean) {
return JsonWriter.toJson(value);
}
if (value instanceof List<?>) {
return "<array length=" + ((List<?>) value).size() + ">";
}
if (value instanceof Map<?, ?>) {
return "<object>";
}
return String.valueOf(value);
}
private static String trimNumber(double value) {
if (Math.rint(value) == value) {
return String.valueOf((long) value);
}
return String.valueOf(value);
}
private static Object loadJson(Path filePath, String label) {
final String text;
try {
text = Files.readString(filePath, StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new RuntimeException(label + " file read error (" + filePath + "): " + ex.getMessage());
}
try {
return JsonParser.parse(text);
} catch (JsonParseException ex) {
throw new RuntimeException(
label + " JSON parse error (" + filePath + "): line " + ex.line + ", col " + ex.column + ": " + ex.getMessage()
);
}
}
private static void writeJson(Path path, Object value) throws IOException {
Files.writeString(path, JsonWriter.toPrettyJson(value) + System.lineSeparator(), StandardCharsets.UTF_8);
}
private static Map<String, Object> generateSampleSchema() {
Map<String, Object> root = new LinkedHashMap<>();
root.put("type", "object");
root.put("required", list("users", "status", "metadata", "note"));
Map<String, Object> properties = new LinkedHashMap<>();
root.put("properties", properties);
Map<String, Object> users = new LinkedHashMap<>();
users.put("type", "array");
users.put("minItems", 1.0);
properties.put("users", users);
Map<String, Object> userItem = new LinkedHashMap<>();
userItem.put("type", "object");
userItem.put("required", list("name", "age", "email", "tags", "active"));
users.put("items", userItem);
Map<String, Object> userProperties = new LinkedHashMap<>();
userItem.put("properties", userProperties);
userProperties.put("name", mapOf("type", "string", "minLength", 3.0, "maxLength", 20.0));
userProperties.put("age", mapOf("type", "number", "min", 0.0, "max", 120.0));
userProperties.put(
"email",
mapOf("type", "string", "pattern", "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
);
userProperties.put("active", mapOf("type", "boolean"));
Map<String, Object> tags = new LinkedHashMap<>();
tags.put("type", "array");
tags.put("minItems", 1.0);
tags.put("items", mapOf("type", "string", "minLength", 2.0, "maxLength", 12.0));
userProperties.put("tags", tags);
properties.put("status", mapOf("type", "string", "enum", list("new", "active", "disabled")));
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("type", "object");
metadata.put("required", list("version", "source"));
properties.put("metadata", metadata);
Map<String, Object> metadataProperties = new LinkedHashMap<>();
metadata.put("properties", metadataProperties);
metadataProperties.put("version", mapOf("type", "string", "pattern", "^\\d+\\.\\d+\\.\\d+$"));
Map<String, Object> source = new LinkedHashMap<>();
source.put("type", "object");
source.put("required", list("systemId"));
source.put("properties", mapOf("systemId", mapOf("type", "string", "minLength", 5.0)));
metadataProperties.put("source", source);
properties.put("note", mapOf("type", "null"));
return root;
}
private static Map<String, Object> generateSampleData() {
Map<String, Object> root = new LinkedHashMap<>();
List<Object> users = new ArrayList<>();
root.put("users", users);
users.add(mapOf(
"name", "Alice",
"age", 29.0,
"email", "alice@example.com",
"tags", list("owner", "ops"),
"active", true
));
users.add(mapOf(
"name", "Al",
"age", -5.0,
"email", "invalid-email",
"tags", list("ok", 5.0, ""),
"active", "true"
));
users.add(mapOf(
"name", 123.0,
"age", 200.0,
"tags", list(),
"active", false
));
root.put("status", "archived");
root.put("metadata", mapOf(
"version", "1.0",
"source", mapOf("systemId", 42.0)
));
root.put("note", "should be null");
return root;
}
@SafeVarargs
private static <T> List<T> list(T... values) {
List<T> list = new ArrayList<>();
Collections.addAll(list, values);
return list;
}
private static Map<String, Object> mapOf(Object... args) {
Map<String, Object> map = new LinkedHashMap<>();
for (int i = 0; i < args.length; i += 2) {
map.put((String) args[i], args[i + 1]);
}
return map;
}
private static final class ValidationError {
final String path;
final String rule;
final String expected;
final String actual;
ValidationError(String path, String rule, String expected, String actual) {
this.path = path;
this.rule = rule;
this.expected = expected;
this.actual = actual;
}
}
private static final class JsonParser {
private final String text;
private int index;
private int line;
private int column;
private JsonParser(String text) {
this.text = text;
this.index = 0;
this.line = 1;
this.column = 1;
}
static Object parse(String text) {
JsonParser parser = new JsonParser(text);
parser.skipWhitespace();
Object value = parser.parseValue();
parser.skipWhitespace();
if (!parser.isEnd()) {
parser.error("Unexpected trailing content");
}
return value;
}
private Object parseValue() {
if (isEnd()) error("Unexpected end of input");
char ch = current();
if (ch == '{') return parseObject();
if (ch == '[') return parseArray();
if (ch == '"') return parseString();
if (ch == 't') return parseLiteral("true", Boolean.TRUE);
if (ch == 'f') return parseLiteral("false", Boolean.FALSE);
if (ch == 'n') return parseLiteral("null", null);
if (ch == '-' || Character.isDigit(ch)) return parseNumber();
error("Unexpected character '" + ch + "'");
return null;
}
private Map<String, Object> parseObject() {
expect('{');
skipWhitespace();
Map<String, Object> object = new LinkedHashMap<>();
if (tryConsume('}')) return object;
while (true) {
skipWhitespace();
if (current() != '"') error("Object keys must be strings");
String key = parseString();
skipWhitespace();
expect(':');
skipWhitespace();
Object value = parseValue();
object.put(key, value);
skipWhitespace();
if (tryConsume('}')) break;
expect(',');
}
return object;
}
private List<Object> parseArray() {
expect('[');
skipWhitespace();
List<Object> array = new ArrayList<>();
if (tryConsume(']')) return array;
while (true) {
skipWhitespace();
array.add(parseValue());
skipWhitespace();
if (tryConsume(']')) break;
expect(',');
}
return array;
}
private String parseString() {
expect('"');
StringBuilder sb = new StringBuilder();
while (!isEnd()) {
char ch = current();
if (ch == '"') {
advance();
return sb.toString();
}
if (ch == '\\') {
advance();
if (isEnd()) error("Unterminated escape sequence");
char esc = current();
switch (esc) {
case '"':
case '\\':
case '/':
sb.append(esc);
advance();
break;
case 'b':
sb.append('\b');
advance();
break;
case 'f':
sb.append('\f');
advance();
break;
case 'n':
sb.append('\n');
advance();
break;
case 'r':
sb.append('\r');
advance();
break;
case 't':
sb.append('\t');
advance();
break;
case 'u':
advance();
sb.append(parseUnicodeEscape());
break;
default:
error("Invalid escape character '" + esc + "'");
}
} else {
if (ch < 0x20) error("Unescaped control character in string");
sb.append(ch);
advance();
}
}
error("Unterminated string");
return null;
}
private char parseUnicodeEscape() {
if (index + 4 > text.length()) error("Incomplete unicode escape");
String hex = text.substring(index, index + 4);
for (int i = 0; i < hex.length(); i++) {
char ch = hex.charAt(i);
if (!Character.isDigit(ch) && (Character.toLowerCase(ch) < 'a' || Character.toLowerCase(ch) > 'f')) {
error("Invalid unicode escape");
}
}
int code = Integer.parseInt(hex, 16);
advance(4);
return (char) code;
}
private Object parseLiteral(String literal, Object value) {
for (int i = 0; i < literal.length(); i++) {
if (isEnd() || current() != literal.charAt(i)) {
error("Unexpected token");
}
advance();
}
return value;
}
private Number parseNumber() {
int start = index;
if (current() == '-') advance();
if (isEnd()) error("Invalid number");
if (current() == '0') {
advance();
} else if (Character.isDigit(current())) {
while (!isEnd() && Character.isDigit(current())) advance();
} else {
error("Invalid number");
}
if (!isEnd() && current() == '.') {
advance();
if (isEnd() || !Character.isDigit(current())) error("Invalid decimal number");
while (!isEnd() && Character.isDigit(current())) advance();
}
if (!isEnd() && (current() == 'e' || current() == 'E')) {
advance();
if (!isEnd() && (current() == '+' || current() == '-')) advance();
if (isEnd() || !Character.isDigit(current())) error("Invalid exponent");
while (!isEnd() && Character.isDigit(current())) advance();
}
String raw = text.substring(start, index);
try {
return Double.parseDouble(raw);
} catch (NumberFormatException ex) {
error("Invalid number format");
return null;
}
}
private void skipWhitespace() {
while (!isEnd()) {
char ch = current();
if (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t') {
advance();
} else {
break;
}
}
}
private boolean tryConsume(char expected) {
if (!isEnd() && current() == expected) {
advance();
return true;
}
return false;
}
private void expect(char expected) {
if (isEnd() || current() != expected) {
error("Expected '" + expected + "'");
}
advance();
}
private char current() {
return text.charAt(index);
}
private boolean isEnd() {
return index >= text.length();
}
private void advance() {
advance(1);
}
private void advance(int count) {
for (int i = 0; i < count; i++) {
if (isEnd()) return;
char ch = text.charAt(index++);
if (ch == '\n') {
line += 1;
column = 1;
} else {
column += 1;
}
}
}
private void error(String message) {
throw new JsonParseException(message, line, column);
}
}
private static final class JsonWriter {
static String toJson(Object value) {
StringBuilder sb = new StringBuilder();
write(value, sb, false, 0);
return sb.toString();
}
static String toPrettyJson(Object value) {
StringBuilder sb = new StringBuilder();
write(value, sb, true, 0);
return sb.toString();
}
@SuppressWarnings("unchecked")
private static void write(Object value, StringBuilder sb, boolean pretty, int indent) {
if (value == null) {
sb.append("null");
return;
}
if (value instanceof String) {
sb.append('"').append(escape((String) value)).append('"');
return;
}
if (value instanceof Boolean) {
sb.append(value);
return;
}
if (value instanceof Number) {
double d = ((Number) value).doubleValue();
if (Double.isFinite(d)) {
if (Math.rint(d) == d) {
sb.append((long) d);
} else {
sb.append(d);
}
} else {
sb.append("null");
}
return;
}
if (value instanceof List<?>) {
List<Object> list = (List<Object>) value;
sb.append('[');
if (!list.isEmpty()) {
if (pretty) sb.append('\n');
for (int i = 0; i < list.size(); i++) {
if (pretty) indent(sb, indent + 1);
write(list.get(i), sb, pretty, indent + 1);
if (i < list.size() - 1) sb.append(',');
if (pretty) sb.append('\n');
}
if (pretty) indent(sb, indent);
}
sb.append(']');
return;
}
if (value instanceof Map<?, ?>) {
Map<String, Object> map = (Map<String, Object>) value;
sb.append('{');
if (!map.isEmpty()) {
if (pretty) sb.append('\n');
int index = 0;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (pretty) indent(sb, indent + 1);
sb.append('"').append(escape(entry.getKey())).append('"');
sb.append(pretty ? ": " : ":");
write(entry.getValue(), sb, pretty, indent + 1);
if (index < map.size() - 1) sb.append(',');
if (pretty) sb.append('\n');
index++;
}
if (pretty) indent(sb, indent);
}
sb.append('}');
return;
}
sb.append('"').append(escape(String.valueOf(value))).append('"');
}
private static void indent(StringBuilder sb, int level) {
for (int i = 0; i < level; i++) sb.append(" ");
}
private static String escape(String text) {
StringBuilder out = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
switch (ch) {
case '"':
out.append("\\\"");
break;
case '\\':
out.append("\\\\");
break;
case '\b':
out.append("\\b");
break;
case '\f':
out.append("\\f");
break;
case '\n':
out.append("\\n");
break;
case '\r':
out.append("\\r");
break;
case '\t':
out.append("\\t");
break;
default:
if (ch < 0x20) {
out.append(String.format("\\u%04x", (int) ch));
} else {
out.append(ch);
}
}
}
return out.toString();
}
}
private static final class JsonParseException extends RuntimeException {
final int line;
final int column;
JsonParseException(String message, int line, int column) {
super(message);
this.line = line;
this.column = column;
}
}
}