JQuick JSON
JQuick JSON
Section titled “JQuick JSON”Overview
Section titled “Overview”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).
✨ Core Features
Section titled “✨ Core Features”- 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:
JSONObjectimplementsMap<String, Object>andJSONArrayimplementsList<Object>, backed byLinkedHashMap/ArrayList— use them as ordinary Map / List instances. - Annotation-driven field mapping:
@JSONField(name, format)renames keys and applies formatting,@JSONIgnoreexcludes a field, andstaticfields are skipped automatically. - Variable rendering:
${...}placeholders in field values are resolved from aJContextduring serialization — ideal for templated or configuration-driven JSON. - Multi-strategy JSON merging: built-in deep and shallow merge, plus
JCustomMergeStrategyfor user-defined merge logic. - Complete utility set:
prettyPrint, lightweightisValidJsonvalidation, generictoJsonStringserialization 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/deserializeandfromBean/toBean, keeping learning and migration costs low.
🏗️ Architecture
Section titled “🏗️ Architecture”Serialization pipeline
Section titled “Serialization pipeline”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 stringA field whose value is null is emitted as null by default; fields annotated with @JSONIgnore and static fields never appear in the output.
Deserialization pipeline
Section titled “Deserialization pipeline”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 returnedSyntax 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.
Bytecode accessor & metadata cache
Section titled “Bytecode accessor & metadata cache”JBeanAccessorFactory holds a ConcurrentHashMap<Class<?>, JBeanAccessor>:
- On the first encounter with a type, it collects all fields across the class hierarchy (subclass first, superclass after, excluding
Object) and resolves@JSONField/@JSONIgnoremetadata; - 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>; - If any precondition fails or generation fails (
RuntimeException/LinkageError), it logs a fallback message and usesJReflectionBeanAccessor; - The accessor and
JBeanFieldMetametadata are cached permanently per type — zero repeated resolution on later serializations/deserializations.
🚀 Quick Start
Section titled “🚀 Quick Start”Requirements: JDK 8 or higher (JDK 8 / 11 / 17 / 21 all work).
Maven & Gradle
Section titled “Maven & Gradle”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'Serialize objects
Section titled “Serialize objects”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} }}Deserialize objects
Section titled “Deserialize objects”JSONSerializer serializer = JSONSerializerFactory.getDefaultSerializer();
String json = "{\"name\":\"John\",\"age\":30}";User user = serializer.deserialize(json, User.class);System.out.println(user.getName()); // JohnPassing 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);Pretty printing
Section titled “Pretty printing”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}🧩 API & annotations
Section titled “🧩 API & annotations”Serializer entry points
Section titled “Serializer entry points”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 |
The JSONSerializer interface
Section titled “The JSONSerializer interface”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 & JSONArray
Section titled “JSONObject & JSONArray”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.
JSONSupport utilities
Section titled “JSONSupport utilities”| 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 |
Built-in annotations
Section titled “Built-in annotations”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 |
⚙️ Advanced features
Section titled “⚙️ Advanced features”Date & number formatting
Section titled “Date & number formatting”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.
Variable rendering with JContext
Section titled “Variable rendering with JContext”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.
Merging JSON documents
Section titled “Merging JSON documents”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 winsJSONObject 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 winJSONObject 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);Enums, nested beans and collections
Section titled “Enums, nested beans and collections”- 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:
fromBeanwraps anyCollectionor array into aJSONArray;deserializecan return an array type or aCollectionsubclass directly; - Typed getters on the model: once you have a
JSONObject, usegetInteger(key),getBigDecimal(key),getDate(key)and friends.
For complex generic collections, see Generic collections and TypeReference.
Benchmark
Section titled “Benchmark”The repository ships a built-in benchmark class that you can run yourself:
mvn test -Dtest=JBeanAccessorBenchmarkTestThe 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.
🔁 Migration guide
Section titled “🔁 Migration guide”Migrating from FastJSON
Section titled “Migrating from FastJSON”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
SerializerFeatureconfiguration object: key names, formatting and exclusion are entirely annotation-driven; - Null fields are emitted as
nullby default with no global switch; use@JSONIgnoreon fields that should not be output; - Malformed JSON does not throw: the error is written to
System.erranddeserializereturnsnull— add null checks after migration; - A raw
serialize(String)returns the trimmed bare text; useJSONSupport.toJsonStringfor string literals; - FastJSON features such as
SerializerFeature.WriteMapNullValue, circular-reference detection and$refoutput are not provided.
Migrating from Jackson / Spring Boot
Section titled “Migrating from Jackson / Spring Boot”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;
@Configurationpublic class WebJsonConfig {
@Bean public HttpMessageConverter<Object> jquickJsonHttpMessageConverter() { return new JQuickJsonHttpMessageConverter(); }}❓ FAQ & troubleshooting
Section titled “❓ FAQ & troubleshooting”The first serialization of a POJO is slow
Section titled “The first serialization of a POJO is slow”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.
Date parsing fails
Section titled “Date parsing fails”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.
Generic collections and TypeReference
Section titled “Generic collections and TypeReference”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));}Null fields and ignored fields
Section titled “Null fields and ignored fields”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.
Open source
Section titled “Open source”Released under the Apache License 2.0; commercial use is allowed.
- GitHub repository: https://github.com/paohaijiao/jquick-json
- Issues: https://github.com/paohaijiao/jquick-json/issues
- Maven Central:
io.github.paohaijiao:jquick-json:1.5.0 - Version history:
1.2.0(parsing/generation, JSON merge, variable context, Maven Central release) →1.3.0(Chinese docs) →1.4.0(bytecode accessors, reflection fallback, metadata caching) →1.5.0(javelin 2.7.0, jquick-asm 1.3.0)