com.eatthepath.json.JsonParser Maven / Gradle / Ivy
package com.eatthepath.json;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* An extremely simple JSON parser that interprets JSON objects as Java primitives. JSON types are mapped to Java
* types as follows:
*
*
* Mapping of JSON types to Java types
*
*
*
* JSON type
* Java type
*
*
*
*
*
* string
* {@link String}
*
*
*
* number
* {@link Number} ({@link Long} or {@link Double})
*
*
*
* object
* {@link Map}<{@link String}, {@link Object}>
*
*
*
* array
* {@link List}<{@link Object}>
*
*
*
* boolean
* boolean
*
*
*
* null
* null
*
*
*
*
* {@code JsonParser} instances are not thread-safe; only one thread may safely use a single parser at a
* time, but parsers can be reused (i.e. callers do not need to construct a new parser for each parsing operation).
*
* @author Jon Chambers
*
* @since 0.14.1
*/
public class JsonParser {
private transient String jsonString;
private transient int position;
private transient final StringBuilder stringBuilder = new StringBuilder();
/**
* Constructs a new JSON parser. Parsers are not thread-safe, but may be reused.
*/
public JsonParser() {}
/**
* Parses the given JSON string as a JSON object.
*
* @param jsonString the JSON string to parse
*
* @return a Map representing the JSON object
*
* @throws ParseException if the given JSON string could not be parsed as a JSON object for any reason
*/
public Map parseJsonObject(final String jsonString) throws ParseException {
this.jsonString = jsonString;
this.position = findNextToken(jsonString, 0);
if (jsonString.charAt(position) != '{') {
throw new ParseException("JSON string does not represent a JSON object.", position);
}
return parseObject();
}
/**
* Parses and returns the JSON value (which may be of any JSON type) at the current read position and advances the
* read position to the character after the end of the value.
*
* @return the value at the current read position
*/
Object parseNextValue() throws ParseException {
position = findNextToken(jsonString, position);
final char c = jsonString.charAt(position);
final Object value;
if (c == '"') {
value = parseString();
} else if (c == '-' || (c >= '0' && c <= '9')) {
final int numberStart = position;
try {
value = parseNumber();
} catch (final NumberFormatException e) {
throw new ParseException("Could not parse number", numberStart);
}
} else if (c == '{') {
value = parseObject();
} else if (c == '[') {
value = parseList();
} else if (c == 't') {
if (jsonString.regionMatches(position, "true", 0, 4)) {
value = true;
position += 4;
} else {
throw new ParseException("Unexpected literal", position);
}
} else if (c == 'f') {
if (jsonString.regionMatches(position, "false", 0, 5)) {
value = false;
position += 5;
} else {
throw new ParseException("Unexpected literal", position);
}
} else if (c == 'n') {
if (jsonString.regionMatches(position, "null", 0, 4)) {
value = null;
position += 4;
} else {
throw new ParseException("Unexpected literal", position);
}
} else {
throw new ParseException("Unexpected token", position);
}
return value;
}
/**
* Parses and returns the string at the current read position and advances the read position to the character after
* the end of the string.
*
* @return the string at the current read position
*/
String parseString() throws ParseException {
// Reset and reuse the string builder to avoid unnecessary allocations
stringBuilder.setLength(0);
// Skip the opening quote
position += 1;
int start = position;
for (; position < jsonString.length(); position++) {
final char c = jsonString.charAt(position);
if (c == '\\') {
final int charsConsumed;
final char escapedChar;
if (position + 1 >= jsonString.length()) {
throw new ParseException("Expected escaped character, but reached end of input", position);
}
switch (jsonString.charAt(position + 1)) {
case '\"': {
charsConsumed = 1;
escapedChar = '\"';
break;
}
case '\\': {
charsConsumed = 1;
escapedChar = '\\';
break;
}
case '/': {
charsConsumed = 1;
escapedChar = '/';
break;
}
case 'b': {
charsConsumed = 1;
escapedChar = '\b';
break;
}
case 'f': {
charsConsumed = 1;
escapedChar = '\f';
break;
}
case 'n': {
charsConsumed = 1;
escapedChar = '\n';
break;
}
case 'r': {
charsConsumed = 1;
escapedChar = '\r';
break;
}
case 't': {
charsConsumed = 1;
escapedChar = '\t';
break;
}
case 'u': {
if (position + 5 >= jsonString.length()) {
throw new ParseException("Expected escaped unicode sequence, but reached end of input", position);
}
charsConsumed = 5;
escapedChar = (char) Integer.parseInt(jsonString.substring(position + 2, position + 6), 16);
break;
}
default: {
throw new ParseException("Illegal escaped character: " + jsonString.charAt(position + 1), position);
}
}
stringBuilder.append(jsonString, start, position);
stringBuilder.append(escapedChar);
position += charsConsumed;
start = position + 1;
} else if (c == '"') {
// If the string builder is empty, we didn't wind up un-escaping anything and can just return a
// substring to save an array copy (original string -> returned string as opposed to original string ->
// string builder -> returned string).
final String unescapedString;
if (stringBuilder.length() == 0) {
unescapedString = jsonString.substring(start, position);
} else {
stringBuilder.append(jsonString, start, position);
unescapedString = stringBuilder.toString();
}
// Position the read index after the end of the string
position += 1;
return unescapedString;
}
}
throw new ParseException("Expected \", but reached end of input.", position);
}
/**
* Parses and returns the number at the current read position and advances the read position to the character after
* the end of the number token.
*
* @return the number at the current read position
*/
Number parseNumber() {
final boolean negate = jsonString.charAt(position) == '-';
boolean parseAsDouble = false;
final int startInclusive = position;
int endExclusive;
long parsedLong = 0;
for (endExclusive = negate ? position + 1 : position; endExclusive < jsonString.length(); endExclusive++) {
final char c = jsonString.charAt(endExclusive);
if (c >= '0' && c <= '9') {
parsedLong *= 10;
parsedLong += c - '0';
} else if (c == '.' || c == 'e' || c == 'E' || c == '+' || (c == '-')) {
parseAsDouble = true;
} else {
break;
}
}
position = endExclusive;
if (parseAsDouble) {
return Double.parseDouble(jsonString.substring(startInclusive, endExclusive));
} else {
return negate ? -parsedLong : parsedLong;
}
}
/**
* Parses and returns the object at the current read position and advances the read position to the character after
* the end of the object.
*
* @return the object at the current read position
*/
Map parseObject() throws ParseException {
// Advance past the opening bracket
position = findNextToken(jsonString, position + 1);
final Map map = new HashMap<>();
while (jsonString.charAt(position) != '}') {
if (jsonString.charAt(position) != '"') {
throw new ParseException("Expected a string", position);
}
final String key = parseString();
position = findNextToken(jsonString, position);
if (jsonString.charAt(position) != ':') {
throw new ParseException("Expected ':'", position);
}
// Advance past the key/value separator
position += 1;
map.put(key, parseNextValue());
position = findNextToken(jsonString, position);
if (jsonString.charAt(position) == ',') {
// Position the cursor at the start of the token after the comma
position = findNextToken(jsonString, position + 1);
}
}
// Advance past the closing bracket
position += 1;
return map;
}
/**
* Parses and returns the list at the current read position and advances the read position to the character after
* the end of the list.
*
* @return the list at the current read position
*/
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy