Skip to content

Instantly share code, notes, and snippets.

@apangin
Last active July 22, 2024 20:25
Show Gist options
  • Save apangin/c30d6737c4ae0afe8811d5879dbad287 to your computer and use it in GitHub Desktop.
Save apangin/c30d6737c4ae0afe8811d5879dbad287 to your computer and use it in GitHub Desktop.
Simplified JSON reader
/*
* Copyright Andrei Pangin
* SPDX-License-Identifier: Apache-2.0
*/
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Beware: this is NOT a complete and fully compliant JSON parser!
* Its main purpose is to decode typical simple queries without third-party dependencies.
*/
public class JsonReader {
protected byte[] array;
protected int offset;
protected int next;
public JsonReader(byte[] array) {
this(array, 0);
}
public JsonReader(byte[] array, int offset) {
this.array = array;
this.offset = offset;
skipWhitespace();
}
protected int read() {
int b = next;
next = offset < array.length ? array[offset++] & 0xff : -1;
return b;
}
public final int next() {
return next;
}
public final int skipWhitespace() {
while (next <= ' ' && next != -1) {
read();
}
return next;
}
public final IllegalArgumentException exception(String message) {
return new IllegalArgumentException(message + " at " + offset);
}
public final void expect(int b, String message) {
if (read() != b) {
throw exception(message);
}
}
public final boolean readBoolean() {
int b = read();
if (b == 't' && read() == 'r' && read() == 'u' && read() == 'e') {
return true;
} else if (b == 'f' && read() == 'a' && read() == 'l' && read() == 's' && read() == 'e') {
return false;
}
throw exception("Expected boolean");
}
public final byte readByte() {
return Byte.parseByte(readNumber());
}
public final short readShort() {
return Short.parseShort(readNumber());
}
public final char readChar() {
return readString().charAt(0);
}
public final int readInt() {
return Integer.parseInt(readNumber());
}
public final long readLong() {
return Long.parseLong(readNumber());
}
public float readFloat() {
return Float.parseFloat(readNumber());
}
public final double readDouble() {
return Double.parseDouble(readNumber());
}
public final String readNumber() {
StringBuilder sb = new StringBuilder();
// Sign
if (next == '-') {
sb.append((char) read());
}
// Integer
int nochars = sb.length();
while (next >= '0' && next <= '9') {
sb.append((char) read());
}
// Fraction
if (next == '.') {
sb.append((char) read());
if (!(next >= '0' && next <= '9')) throw exception("Expected number");
do {
sb.append((char) read());
} while (next >= '0' && next <= '9');
}
if (sb.length() <= nochars) {
throw exception("Expected number");
}
// Exponent
if (next == 'e' || next == 'E') {
sb.append((char) read());
if (next == '-' || next == '+') {
sb.append((char) read());
}
if (!(next >= '0' && next <= '9')) throw exception("Expected number");
do {
sb.append((char) read());
} while (next >= '0' && next <= '9');
}
return sb.toString();
}
public final int readHexChar() {
int b = read();
if (b >= '0' && b <= '9') {
return b - '0';
} else if (b >= 'A' && b <= 'F') {
return b - 'A';
} else if (b >= 'a' && b <= 'f') {
return b - 'a';
}
throw exception("Invalid escape character");
}
public final char readEscapeChar() {
int b = read();
switch (b) {
case 'b':
return '\b';
case 'f':
return '\f';
case 'n':
return '\n';
case 'r':
return '\r';
case 't':
return '\t';
case 'u':
return (char) (readHexChar() << 12 | readHexChar() << 8 | readHexChar() << 4 | readHexChar());
default:
return (char) b;
}
}
public Object readNull() {
if (read() != 'n' || read() != 'u' || read() != 'l' || read() != 'l') {
throw exception("Expected null");
}
return null;
}
public String readString() {
StringBuilder sb = new StringBuilder();
expect('\"', "Expected string");
while (next >= 0 && next != '\"') {
int b = read();
if ((b & 0x80) == 0) {
sb.append(b == '\\' ? readEscapeChar() : (char) b);
} else if ((b & 0xe0) == 0xc0) {
sb.append((char) ((b & 0x1f) << 6 | (read() & 0x3f)));
} else if ((b & 0xf0) == 0xe0) {
sb.append((char) ((b & 0x0f) << 12 | (read() & 0x3f) << 6 | (read() & 0x3f)));
} else {
int v = (b & 0x07) << 18 | (read() & 0x3f) << 12 | (read() & 0x3f) << 6 | (read() & 0x3f);
sb.append((char) (0xd800 | (v - 0x10000) >>> 10)).append((char) (0xdc00 | (v & 0x3ff)));
}
}
expect('\"', "Unexpected end of string");
return sb.toString();
}
public ArrayList<Object> readArray() {
ArrayList<Object> result = new ArrayList<>();
expect('[', "Expected array");
for (boolean needComma = false; skipWhitespace() != ']'; needComma = true) {
if (needComma) {
expect(',', "Unexpected end of array");
skipWhitespace();
}
result.add(readObject());
}
read();
return result;
}
public Map<String, Object> readMap() {
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
expect('{', "Expected map");
for (boolean needComma = false; skipWhitespace() != '}'; needComma = true) {
if (needComma) {
expect(',', "Unexpected end of map");
skipWhitespace();
}
String key = readString();
skipWhitespace();
expect(':', "Expected key-value pair");
skipWhitespace();
result.put(key, readObject());
}
read();
return result;
}
public Object readObject() {
switch (next) {
case 'n':
return readNull();
case 'f':
case 't':
return readBoolean();
case '\"':
return readString();
case '[':
return readArray();
case '{':
return readMap();
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '-':
case '.':
return parseNumber(readNumber());
}
throw exception("Expected JSON object");
}
private static Number parseNumber(String number) {
int length = number.length();
for (int i = 0; i < length; i++) {
char c = number.charAt(i);
if (c == '.' || c > '9') {
return Double.parseDouble(number);
}
}
long n = Long.parseLong(number);
return n == (int) n ? (int) n : (Number) n;
}
}
import java.nio.charset.StandardCharsets;
public class JsonReaderTest {
static final String SAMPLE = "[{\n" +
" \"created_at\": \"Thu Jun 22 21:00:00 +0000 2017\",\n" +
" \"id\": 877994604561387500,\n" +
" \"id_str\": \"877994604561387520\",\n" +
" \"text\": \"Creating a Grocery List Manager \\u0026 Display Items https://t.co/xFox12345 #Angular\",\n" +
" \"truncated\": false,\n" +
" \"entities\": {\n" +
" \"hashtags\": [{\n" +
" \"text\": \"Angular\",\n" +
" \"indices\": [103, 111]\n" +
" }],\n" +
" \"symbols\": [null],\n" +
" \"user_mentions\": [],\n" +
" \"urls\": [{\n" +
" \"url\": \"https://t.co/xFox12345\",\n" +
" \"expanded_url\": \"http://example.com/2sr60pf\",\n" +
" \"display_url\": \"example.com/2sr60pf\",\n" +
" \"indices\": [79, 102]\n" +
" }]\n" +
" },\n" +
" \"source\": \"<a href=\\\"http://example.com\\\" rel=\\\"nofollow\\\">Some link</a>\",\n" +
" \"user\": {\n" +
" \"id\": 772682964,\n" +
" \"id_str\": \"772682964\",\n" +
" \"name\": \"Example JavaScript\",\n" +
" \"screen_name\": \"ExampleJS\",\n" +
" \"location\": \"Melbourne, Australia\",\n" +
" \"description\": \"Keep up with JavaScript tutorials, tips, tricks and articles.\",\n" +
" \"url\": \"http://t.co/cCHxxxxx\",\n" +
" \"entities\": {\n" +
" \"url\": {\n" +
" \"urls\": [{\n" +
" \"url\": \"http://t.co/cCHxxxxx\",\n" +
" \"expanded_url\": \"http://example.com/javascript\",\n" +
" \"display_url\": \"example.com/javascript\",\n" +
" \"indices\": [0, 22]\n" +
" }]\n" +
" },\n" +
" \"description\": {\n" +
" \"urls\": []\n" +
" }\n" +
" },\n" +
" \"protected\": false,\n" +
" \"followers_count\": 2145,\n" +
" \"friends_count\": 18,\n" +
" \"listed_count\": 328,\n" +
" \"created_at\": \"Wed Aug 22 02:06:33 +0000 2012\",\n" +
" \"favourites_count\": 57,\n" +
" \"utc_offset\": 43200,\n" +
" \"time_zone\": \"Wellington\"\n" +
" }\n" +
"}]";
public static void main(String[] args) throws Exception {
Object o = new JsonReader(SAMPLE.getBytes(StandardCharsets.UTF_8)).readObject();
System.out.println(o);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment