Skip to content

JQuick JSON

jquick-json is a fully self-developed, lightweight and high-performance Java JSON parsing and serialization library in the JQuick ecosystem. It uses hash maps as the core storage and offers a concise, efficient API for JSON parsing, generation, two-way POJO conversion, variable rendering and document merging. Since 1.4.0, POJO conversion is driven by runtime-generated bytecode accessors.

The library is pure Java and targets JDK 8+. JSONObject / JSONArray are backed by LinkedHashMap / ArrayList and implement the standard Map / List interfaces — there is no proprietary data model. The parser is driven by self-developed grammar rules and does not bind to Spring or any other container; it works in plain Java projects and Spring / Spring Boot applications alike.

Maven coordinates: io.github.paohaijiao:jquick-json:1.5.0 (latest release on Maven Central).

  • Fully self-developed, grammar-driven parsing: a self-developed lexer, parser and visitor with full support for objects, arrays, strings, integers, floats, booleans and null.
  • Bytecode bean accessors: since 1.4.0, the first use of a type triggers jquick-asm to generate an accessor class at runtime, compiling field reads/writes into direct getter/setter calls (INVOKEVIRTUAL) instead of per-field reflection.
  • Per-type caching with reflection fallback: the accessor and field metadata are generated/resolved once per type and cached globally; interfaces, abstract classes, types without a public no-arg constructor or without getters/setters fall back to reflection with identical behavior.
  • Standard data model: JSONObject implements Map<String, Object> and JSONArray implements List<Object>, backed by LinkedHashMap / ArrayList — use them as ordinary Map / List instances.
  • Annotation-driven field mapping: @JSONField(name, format) renames keys and applies formatting, @JSONIgnore excludes a field, and static fields are skipped automatically.
  • Variable rendering: ${...} placeholders in field values are resolved from a JContext during serialization — ideal for templated or configuration-driven JSON.
  • Multi-strategy JSON merging: built-in deep and shallow merge, plus JCustomMergeStrategy for user-defined merge logic.
  • Complete utility set: prettyPrint, lightweight isValidJson validation, generic toJsonString serialization and escaping helpers.
  • Broad type coverage: plain POJOs, nested beans, collections, arrays, Date, BigDecimal, all boxed types and enum-by-name conversion.
  • Concise API: the core surface is just serialize / deserialize and fromBean / toBean, keeping learning and migration costs low.
POJO instance
→ JBeanAccessorFactory.get(type) // cache lookup: bytecode or reflection accessor
→ read JBeanFieldMeta[] field metadata // name, format, ignored, type resolved once
→ ASM accessor invokes getters directly // INVOKEVIRTUAL, no per-field reflection
→ type dispatch (Date / BigDecimal / collection / nested bean …)
→ put into JSONObject / JSONArray (LinkedHashMap / ArrayList)
→ toString() builds the JSON string

A field whose value is null is emitted as null by default; fields annotated with @JSONIgnore and static fields never appear in the output.

JSON string
→ JQuickJSONLexer (lexer)
→ JQuickJSONParser (parser)
→ JSONCommonVisitor (visits the parse tree)
→ JSONObject / JSONArray / native type
→ JSONObject.toBean(clazz): ASM accessor invokes setters directly
→ target instance returned

Syntax errors are reported through the JSONExecutor error listener to the standard error stream, and deserialize returns null instead of throwing — callers must null-check the result.

JBeanAccessorFactory holds a ConcurrentHashMap<Class<?>, JBeanAccessor>:

  1. On the first encounter with a type, it collects all fields across the class hierarchy (subclass first, superclass after, excluding Object) and resolves @JSONField / @JSONIgnore metadata;
  2. When the type is neither an interface nor abstract, has a public no-arg constructor and every field has a resolvable getter/setter, jquick-asm generates and loads an accessor class named com.github.paohaijiao.mapper.gen.JBeanAccessor_<hash>_<seq>;
  3. If any precondition fails or generation fails (RuntimeException / LinkageError), it logs a fallback message and uses JReflectionBeanAccessor;
  4. The accessor and JBeanFieldMeta metadata are cached permanently per type — zero repeated resolution on later serializations/deserializations.

Requirements: JDK 8 or higher (JDK 8 / 11 / 17 / 21 all work).

Maven:

<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-json</artifactId>
<version>1.5.0</version>
</dependency>

Gradle:

implementation 'io.github.paohaijiao:jquick-json:1.5.0'

The example below compiles and runs as-is:

package demo;
import com.github.paohaijiao.factory.JSONSerializerFactory;
import com.github.paohaijiao.serializer.JSONSerializer;
/**
* Minimal jquick-json example
*/
public class JsonQuickStart {
public static class User {
private String name;
private Integer age;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
public static void main(String[] args) {
JSONSerializer serializer = JSONSerializerFactory.getDefaultSerializer();
String json = serializer.serialize(new User("John", 30));
System.out.println(json); // {"name":"John","age":30}
}
}
JSONSerializer serializer = JSONSerializerFactory.getDefaultSerializer();
String json = "{\"name\":\"John\",\"age\":30}";
User user = serializer.deserialize(json, User.class);
System.out.println(user.getName()); // John

Passing JSONObject.class / JSONArray.class returns the hash model directly; an array type or a Collection subclass returns an array/collection:

JSONObject obj = serializer.deserialize("{\"a\":1}", JSONObject.class);
JSONArray arr = JSONArray.parseJSONArray("[1,2,3]");
Object[] array = serializer.deserialize("[1,2,3]", Object[].class);

JSONSupport.prettyPrint reformats a JSON string with a configurable indentation width:

String json = serializer.serialize(new User("John", 30));
String pretty = JSONSupport.prettyPrint(json, 3);
System.out.println(pretty);

Output:

{
"name" : "John",
"age" : 30
}

JSONSerializerFactory is the single entry point for obtaining a serializer:

Static method Description
getDefaultSerializer() Returns the shared default serializer singleton (with an empty JContext internally)
createJQuickSerializer() Creates a new default serializer instance
createJQuickSerializer(JContext context) Creates a serializer with a variable context for ${...} rendering
public interface JSONSerializer {
String serialize(Object object);
<T> T deserialize(String json, Class<T> clazz);
}

serialize accepts POJOs, JSONObject / JSONArray, Map, Collection, arrays, Number and Boolean; the deserialize target type can be a plain POJO, JSONObject, JSONArray, an array type or a Collection subclass.

JSONObject implements Map<String, Object>. Commonly used methods:

Method Description
new JSONObject() / new JSONObject(Map) Empty object or construction from a Map (insertion order preserved)
fromBean(Object) POJO → JSONObject
toBean(Class<T>) JSONObject → POJO
fromMap(Map) / toMap() Conversion to/from a plain Map (toMap returns a deep copy)
getString / getInteger / getLong / getDouble / getBoolean Typed getters; a missing key returns null
getDate / getBigDecimal / getFloat Typed getters for dates and high-precision numbers
deepMergeWith(other) / shallowMergeWith(other) Deep / shallow merge
mergeWith(other, strategy) Merge with a custom strategy

JSONArray implements List<Object> and additionally provides the static parseJSONArray(String), toCollection(), getJSONObject(index) / getJSONArray(index) and typed getters.

Static method Description
toJsonString(Object) Generic serialization: POJOs go through fromBean, strings are quoted and escaped, and Map/Collection values are wrapped as JSON models
prettyPrint(String json, int indentSpaces) Pretty-prints; non-JSON text is returned unchanged
isValidJson(String) Lightweight check: after trimming, the text must be wrapped in matching {} or []
merge(JSONObject, JSONObject) Simple merge: keys from the second object override the first

Both annotations target fields (@Target(ElementType.FIELD), retained at RUNTIME):

import com.github.paohaijiao.anno.JSONField;
import com.github.paohaijiao.anno.JSONIgnore;
import java.util.Date;
public class User {
@JSONField(name = "user_name")
private String name;
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date createdAt;
@JSONIgnore
private String internalToken;
// getters / setters omitted
}
Annotation Attribute Description
@JSONField name Overrides the JSON key for output/input; defaults to the Java field name
@JSONField format Formats Date, BigDecimal, Float, Double, Integer, Long and Boolean; dates use SimpleDateFormat, numbers use DecimalFormat
@JSONIgnore — Skipped in both serialization and deserialization; static fields are skipped without any annotation

Serialization formatting is declared with @JSONField(format = ...):

public class Order {
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date orderTime; // 2026-10-01 09:30:00
@JSONField(format = "0.00")
private BigDecimal amount; // 199.00
@JSONField(format = "#,###")
private Integer quantity; // 1,200
}

When deserializing a Date field, the string is tried against the built-in patterns in order: yyyy-MM-dd, dd/MM/yyyy, MM-dd-yyyy, yyyy MM dd, yyyy.MM.dd, MMM dd, yyyy.

Write template field values as ${name} and supply the variables through a JContext, then use a context-aware serializer:

import com.github.paohaijiao.param.JContext;
import com.github.paohaijiao.factory.JSONSerializerFactory;
import com.github.paohaijiao.serializer.JSONSerializer;
JUserModel template = new JUserModel("${key}", "${value}");
JContext context = new JContext();
context.put("key", "key1");
context.put("value", "key2");
JSONSerializer serializer = JSONSerializerFactory.createJQuickSerializer(context);
String json = serializer.serialize(template);
System.out.println(json); // {"name":"key1","value":"key2"}

JSONObject also offers a new JSONObject(JContext) constructor, so fromBean carries the same variable context.

Three built-in strategies are available; all return a new object and leave the inputs untouched:

JSONObject first = serializer.deserialize("{\"a\":1,\"nest\":{\"x\":1}}", JSONObject.class);
JSONObject second = serializer.deserialize("{\"b\":2,\"nest\":{\"y\":2}}", JSONObject.class);
// Shallow: top-level keys only, the second object wins
JSONObject shallow = first.shallowMergeWith(second);
// {"a":1,"nest":{"y":2},"b":2}
// Deep: nested JSONObjects are merged recursively; arrays are concatenated; scalar keys from the second win
JSONObject deep = first.deepMergeWith(second);
// {"a":1,"nest":{"x":1,"y":2},"b":2}

For custom rules, use JCustomMergeStrategy with separate merge functions for objects and arrays:

import com.github.paohaijiao.merge.impl.JCustomMergeStrategy;
JCustomMergeStrategy strategy = new JCustomMergeStrategy(
(target, source) -> { /* return the custom-merged JSONObject */ return source; },
(target, source) -> { /* return the custom-merged JSONArray */ return source; }
);
JSONObject merged = first.mergeWith(second, strategy);
  • Nested beans: during toBean, a JSON object mapped to a non-Map POJO field is converted recursively;
  • Enums: deserialization matches enum constants by name (name());
  • Collections / arrays: fromBean wraps any Collection or array into a JSONArray; deserialize can return an array type or a Collection subclass directly;
  • Typed getters on the model: once you have a JSONObject, use getInteger(key), getBigDecimal(key), getDate(key) and friends.

For complex generic collections, see Generic collections and TypeReference.

The repository ships a built-in benchmark class that you can run yourself:

Terminal window
mvn test -Dtest=JBeanAccessorBenchmarkTest

The table below shows the official reference numbers for a 5-field POJO (String / Integer / int / boolean / long) on JDK 1.8.0_191 / Windows 10 (ns/op, nanoseconds per operation):

Scenario Reflection ns/op Bytecode ns/op Speedup
accessor.get (single field read) 12.2 5.3 2.3x
accessor.set (single field write) 53.3 1.0 54.0x
accessor.create (instance creation) 2.6 1.4 1.8x
fromBean (bean → JSONObject) 238.8 225.8 1.1x
toBean (JSONObject → bean) 323.4 34.5 9.4x
serialize full pipeline - 5,780 -
deserialize full pipeline - 19,277 -

Field writes and toBean benefit the most; fromBean is dominated by type dispatch and map building, so its gain is modest. Re-run the benchmark on your target environment for authoritative numbers.

The API naming is close to FastJSON. Common call mappings:

FastJSON jquick-json
JSON.toJSONString(obj) serializer.serialize(obj) or JSONSupport.toJsonString(obj)
JSON.parseObject(json, User.class) serializer.deserialize(json, User.class)
JSON.parseObject(json) serializer.deserialize(json, JSONObject.class)
JSON.parseArray(json) JSONArray.parseJSONArray(json)
new JSONObject(map) / fluentPut Same constructor + standard Map#put
@JSONField(name = "x") @JSONField(name = "x") (same annotation name and meaning)
@JSONField(format = "...") @JSONField(format = "...")
@JSONField(serialize = false) @JSONIgnore

Behavioral differences to account for:

  • There is no global SerializerFeature configuration object: key names, formatting and exclusion are entirely annotation-driven;
  • Null fields are emitted as null by default with no global switch; use @JSONIgnore on fields that should not be output;
  • Malformed JSON does not throw: the error is written to System.err and deserialize returns null — add null checks after migration;
  • A raw serialize(String) returns the trimmed bare text; use JSONSupport.toJsonString for string literals;
  • FastJSON features such as SerializerFeature.WriteMapNullValue, circular-reference detection and $ref output are not provided.

Common API and annotation mappings:

Jackson jquick-json
objectMapper.writeValueAsString(obj) serializer.serialize(obj)
objectMapper.readValue(json, User.class) serializer.deserialize(json, User.class)
@JsonProperty("x") @JSONField(name = "x")
@JsonIgnore @JSONIgnore
@JsonFormat(pattern = "...") @JSONField(format = "...")

The library does not ship a Spring Boot starter. To make jquick-json the default JSON converter in Spring Boot, implement an HttpMessageConverter based on the public API in your application (the code below is application-side code you can copy into your project; it is not a built-in class of the library):

package demo.config;
import com.github.paohaijiao.factory.JSONSerializerFactory;
import com.github.paohaijiao.serializer.JSONSerializer;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* Application-side jquick-json message converter
*/
public class JQuickJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
private final JSONSerializer serializer = JSONSerializerFactory.getDefaultSerializer();
public JQuickJsonHttpMessageConverter() {
super(MediaType.APPLICATION_JSON);
}
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
@Override
protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
String json = new String(inputMessage.getBody().readAllBytes(), StandardCharsets.UTF_8);
return serializer.deserialize(json, clazz);
}
@Override
protected void writeInternal(Object obj, HttpOutputMessage outputMessage) throws IOException {
String json = serializer.serialize(obj);
outputMessage.getBody().write(json.getBytes(StandardCharsets.UTF_8));
}
}

Register it as the highest-priority bean to replace the default Jackson converter:

package demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
@Configuration
public class WebJsonConfig {
@Bean
public HttpMessageConverter<Object> jquickJsonHttpMessageConverter() {
return new JQuickJsonHttpMessageConverter();
}
}

Bytecode accessors are generated lazily: fields are scanned and the accessor class is generated and loaded on the first fromBean / toBean of each type, then cached forever. For services sensitive to startup latency, warm up hot DTOs during startup by calling JBeanAccessorFactory.get(YourDto.class) once (or serializing one instance).

When does the reflection fallback kick in?

Section titled “When does the reflection fallback kick in?”

A type automatically uses JReflectionBeanAccessor — with identical features but reflection-level performance — when any of the following holds: it is an interface or abstract class, it has no public no-arg constructor, any field lacks a spec-compliant getter/setter, or bytecode generation/loading throws. Search the logs for fallback to reflection to confirm.

The built-in deserialization date patterns are fixed: yyyy-MM-dd, dd/MM/yyyy, MM-dd-yyyy, yyyy MM dd, yyyy.MM.dd, MMM dd, yyyy. A string outside this list causes a runtime exception. On the output side, control the format with @JSONField(format = "yyyy-MM-dd HH:mm:ss"); for unusual input formats, accept a String and parse it yourself.

Invalid JSON returns null instead of throwing

Section titled “Invalid JSON returns null instead of throwing”

deserialize writes syntax errors (with line/column and the rule stack) to System.err and returns null. Always null-check the result in production code. JSONSupport.isValidJson only does a lightweight bracket check and cannot replace a full parse.

Due to JVM type erasure, deserialize(json, List.class) yields a collection whose elements are Object (in fact JSONObject). The library provides no TypeReference API — convert complex generics explicitly, element by element:

JSONArray array = JSONArray.parseJSONArray("[{\"name\":\"John\",\"age\":30}]");
List<User> users = new ArrayList<>();
for (Object item : array) {
users.add(new JSONObject((Map<String, Object>) item).toBean(User.class));
}

fromBean emits "key":null for null-valued fields by default; add @JSONIgnore when a field must not appear. static fields are always ignored without any annotation, and @JSONIgnore applies in both directions — serialization and deserialization.

Released under the Apache License 2.0; commercial use is allowed.