| Artifact | io.github.microsphere-projects:microsphere-java-core (0.3.20-SNAPSHOT, release branch) |
| Package | io.microsphere.json — 8 main-source types |
| JDK level | Java 8 bytecode, inherited from the external build parent; CI-tested on JDK 8 / 11 / 17 / 21 / 25 |
| Derived from | a vendored fork of org.json, specifically Android's org.json implementation — json/package-info.java:19 states "Fork the source code from org.json", and JSON.java:2, JSONArray.java:2, JSONException.java:2, JSONObject.java:2, JSONStringer.java:2, JSONTokener.java:2 all carry Copyright (C) 2010 The Android Open Source Project. JSONUtils.java:2-16 and package-info.java:2-16 carry the Apache Software Foundation header and are original Microsphere code. |
Depends on other core packages |
io.microsphere.beans (BeanUtils.resolvePropertiesAsMap, BeanUtils.findWriteMethod, BeanUtils.getBeanMetadata, BeanMetadata), io.microsphere.collection (ListUtils, MapUtils, EnumerationUtils, QueueUtils, SetUtils), io.microsphere.util (ClassUtils, CharSequenceUtils, StringUtils, ExceptionUtils, IterableUtils, Utils), io.microsphere.reflect (ConstructorUtils, MethodUtils, TypeUtils), io.microsphere.convert (Converter.convertIfPossible), io.microsphere.constants (SymbolConstants), io.microsphere.lang.function (ThrowableAction), io.microsphere.annotation.Nonnull / Nullable (separate artifact) |
| External dependencies | none — pure JDK. There is no Jackson, Gson, org.json, jakarta.json or javax.json dependency for this package anywhere in the reactor; microsphere-java-core/pom.xml:22-74 lists only microsphere-java-annotations, two optional logging back-ends and test-scope artifacts. |
| Used by | io.microsphere.metadata (core), microsphere-lang-model, microsphere-annotation-processor. Verified in §1 of the developer page. |
All file:line references in this page are relative to microsphere-java-core/src/main/java/io/microsphere/
(main sources) or microsphere-java-core/src/test/java/io/microsphere/ (tests), read from the release worktree.
After the first mention they are shortened to json/JSONUtils.java:40-55.
Reach for io.microsphere.json when you need a small, zero-dependency JSON object model inside a library that
cannot afford to pull in a third-party JSON stack. The package is a vendored fork of Android's org.json
(json/package-info.java:19), so its shape is the classic JSONObject / JSONArray / JSONTokener /
JSONStringer / JSONException set, plus one original utility class, JSONUtils, that bridges JSON to Java
collections and JavaBeans. Everything else in microsphere-java-core that emits or reads JSON uses these types —
metadata writes configuration-property documents with JSONUtils.append and reads them back with
JSONUtils.readValues (metadata/DefaultConfigurationPropertyGenerator.java:29-30,
metadata/DefaultConfigurationPropertyReader.java:29,43).
Reach for JSONTokener when you parse a stream of values rather than one document — it is the lexer plus the
recursive-descent parser, and nextValue() returns whichever JSON type comes next
(json/JSONTokener.java:102-122).
Reach for JSONUtils when the JSON side should disappear into Java types — readValue(json, Person.class),
readValues(json, List.class, Data.class), readArray(json, String.class), readValueAsMap(jsonObject),
writeValueAsString(anyObject), writeBeanAsString(bean) (json/JSONUtils.java:884, :1041, :1172, :948,
:1235, :1273).
Do not reach for it when:
- You need a production-grade JSON stack. There is no streaming parser, no
JsonPointer, no schema, noBigDecimalpreservation, no configurableMapper, and no type-token generic-binding API beyondreadValues(JSONArray, Type)(json/JSONUtils.java:1074). The parser is the Android one, with Android's leniencies. - You need a strictly RFC-compliant reader.
JSONTokenerdeliberately accepts comments,#-comments, single quotes,=/=>as the key/value separator,;as an element separator,0x/0integer prefixes, and dangling separators that meannull(json/JSONTokener.java:44-62). A successful parse proves nothing about the input. - You need
nullto mean one thing. The model carries two incompatible nulls — the Javanullreference and theJSONObject.NULLsentinel (json/JSONObject.java:101-114, and the warning atjson/JSONArray.java:49-52). See §10. - You need thread safety. Nothing in the package is safe to share; three of the classes say so explicitly
(
json/JSONTokener.java:64-68,json/JSONArray.java:71-74,json/JSONStringer.java:64-68) andJSONObjectis silent about it. - You want a
java.util.Maporjava.util.List.JSONObjectimplements no interface (json/JSONObject.java:83) andJSONArraynone (json/JSONArray.java:76); neither isSerializable. There is noentrySet(), noIteratorover values, noListview. Convert explicitly withJSONUtils.readValueAsMap(json/JSONUtils.java:948-957).
Important
This package is the library's documented exception to the silent-degradation error policy. Elsewhere
microsphere-java returns a neutral value or no-ops; here, parsing and typed getters throw the checked
JSONException (json/JSONException.java:41-46). The exact boundary is in §3.3 — and it is genuinely
inconsistent between the four write/read paths, which is itself the single biggest thing to know about this package.
<dependency>
<groupId>io.github.microsphere-projects</groupId>
<artifactId>microsphere-java-core</artifactId>
<version>0.3.20</version> <!-- newest published tag; 0.3.20-SNAPSHOT is this same line's working version -->
</dependency>There is nothing to add for JSON specifically. io.microsphere.json ships inside microsphere-java-core and drags
in no transitive JSON artifact.
| Fact | Value | Evidence (release) |
|---|---|---|
groupId |
io.github.microsphere-projects |
pom.xml:12 |
artifactId |
microsphere-java-core |
module pom.xml:14 |
| Version property | ${revision}, flattened |
pom.xml:14, :15 |
Value of ${revision} on release |
0.3.20-SNAPSHOT |
pom.xml:54 |
| Newest published tag | 0.3.20 |
release-line table in the group index (docs/current/user-guide/microsphere-java-core/README.md:70) |
| Build parent | microsphere-build:0.3.16 |
pom.xml:6-10 |
| Java level | no maven.compiler.* / <release> property anywhere in the reactor; the bytecode level is inherited from the external parent microsphere-build:0.3.16 |
CI matrix java: [ '8', '11', '17', '21', '25' ], .github/workflows/maven-build.yml:25, distribution: 'temurin' :33 |
| Spring Framework, JDK 17+ | 7.0.9 |
microsphere-java-parent/pom.xml:23 |
Spring Framework, JDK [1.8,17) |
5.3.39 via profile java8-16, activation <jdk>[1.8,17)</jdk> |
microsphere-java-parent/pom.xml:54-62, activation :57 |
| Imported BOMs | microsphere-all-bom:0.3.13 (:22, :33-36), spring-framework-bom (:40-46) |
microsphere-java-parent/pom.xml |
@since of the original types |
1.0.0 |
json/package-info.java:22, json/JSONUtils.java:111 |
@since of the forked types |
absent — JSONObject, JSONArray, JSONTokener, JSONStringer, JSONException, JSON carry no @author/@since at all |
grep of the package: only two @since tags exist |
| Grammar reference in the sources | RFC 4627 | json/JSONTokener.java:29 |
| Escaping reference in the sources | RFC 7159 | json/JSONUtils.java:121-129 |
The whole package compiles and runs on the JDKs in the CI matrix (8, 11, 17, 21, 25) and uses no JDK-internal API,
no sun.*, no Unsafe, no JPMS module-info (there is none anywhere in the repository). Nothing in it reads a
system property or touches Spring, so the java8-16 profile is irrelevant here.
The API surface is the Android org.json surface, which is stable upstream; treat it as additive-only —
your code should not depend on the package-private helpers (json/JSON.java:25 is package-private, and
JSONStringer.Scope at json/JSONStringer.java:81-118 is a package-private nested enum).
This is the table to memorise. "Neutral value" means null, a default, or the input unchanged.
| Path | On malformed / missing input | Evidence |
|---|---|---|
new JSONObject(String) / new JSONArray(String) / JSONTokener.nextValue() |
throws checked JSONException |
json/JSONObject.java:178-180, json/JSONArray.java:140-142, json/JSONTokener.java:102-122; json/JSONObjectTest.java:105-107, json/JSONTokenerTest.java:111-118 |
JSONObject.get(name) / JSONArray.get(index) and every getX |
throws checked JSONException |
json/JSONObject.java:393-399, json/JSONArray.java:333-343; json/JSONObjectTest.java:218-220 |
every optX (opt, optString, optInt, optJSONObject, …) |
never throws — returns null, "", 0, 0L, false, NaN or your fallback |
json/JSONObject.java:407-409, :436-452, :479-495, :521-537, :566-584, :609-625, :652-655, :682-685; json/JSONObjectTest.java:307-339 |
JSONObject.put(name, value) |
throws JSONException on a null name, and on NaN/infinite numbers; removes the key on a null value |
json/JSONObject.java:274-285, :340-350; json/JSONObjectTest.java:174-176, :537-541 |
JSONArray.put(double) / put(int, Object) |
throws JSONException on NaN/infinity |
json/JSONArray.java:189-192, :299-310; json/JSONArrayTest.java:450-461 |
JSONArray.put(Object) |
no check at all — the source admits it | json/JSONArray.java:225-228, admitted at json/JSONObject.java:323-324 |
toString() / toString(int) |
throws unchecked RuntimeException wrapping JSONException |
json/JSONObject.java:762-766, json/JSONArray.java:713-717, lang/function/ThrowableAction.java:79-81 |
JSONUtils.jsonObject(String) / jsonArray(String) |
throws unchecked IllegalArgumentException (cause = the JSONException) |
json/JSONUtils.java:803-812, :843-852 |
JSONUtils.writeValueAsString(Object) |
returns null when the object is not wrappable |
json/JSONUtils.java:1234-1241; json/JSONUtilsTest.java:678-687 |
JSONObject.wrap(Object) |
returns null on any failure, swallowing the exception |
json/JSONObject.java:867-885; json/JSONObjectTest.java:518-522 |
JSONUtils.escape(String) |
returns the input unchanged; null in → null out |
json/JSONUtils.java:1342-1386; json/JSONUtilsTest.java:760-762 |
Warning
Four different failure vocabularies coexist in one package: checked JSONException (parse and getX), unchecked
RuntimeException (toString), unchecked IllegalArgumentException (JSONUtils.jsonObject/jsonArray), and
silent null (wrap, writeValueAsString, readValues on an unsupported target type). If you only catch
JSONException, the last three will get you. §10 items 5, 8, 9 and 11 name the specific unchecked escapes.
| Item | Value | Evidence |
|---|---|---|
| Documented line | release, ${revision} = 0.3.20-SNAPSHOT |
root pom.xml:54 |
| Newest published release | 0.3.20, tagged 2026-10-16 |
the 0.3.20 tag resolves to chore: merge main into release [skip ci], committed 2026-10-16T03:43:20Z |
| Previous release | 0.3.19, tagged 2026-08-30 |
the 0.3.19 tag, committed 2026-08-30T12:51:37Z |
| Changelog source | release-notes.md at the repository root |
The project publishes no CHANGELOG.md; the v0.3.16–v0.3.20 blocks of release-notes.md read Release notes generation failed and carry raw commit lists, so every entry below is quoted as a commit subject and checked against the release sources. |
0.3.20 adds no new API surface for this area — the 0.3.19...0.3.20 range is a hardening release. What it does change, and how far that reaches the sources this page documents:
- Nullability contracts. 1 of 8 main sources in the scope of this page declares
@Nonnull/@Nullable. No commit subject names this area specifically; the library-wide pass34e27823 Refine nullability contracts across core APIscovers it. - No other change in this range touches the area this page covers.
Note
Reading the commit list: JSONUtils is the only annotated source, and no subject in the range names io.microsphere.json; the two serializer cleanups in this release belong to io.microsphere.io.serializer. The JSON layer is behaviourally unchanged on this line.
| # | Type | Visibility | Role | Forked from AOSP? |
|---|---|---|---|---|
| 1 | JSONObject |
public |
ordered name→value map; the NULL sentinel; wrap, quote, numberToString |
yes (json/JSONObject.java:2) |
| 2 | JSONArray |
public |
dense indexed sequence; same getters/coercion as JSONObject |
yes (json/JSONArray.java:2) |
| 3 | JSONTokener |
public |
lexer + recursive-descent parser over one String |
yes (json/JSONTokener.java:2) |
| 4 | JSONStringer |
public |
the only writer for JSONObject/JSONArray; scope stack + indent |
yes (json/JSONStringer.java:2) |
| 5 | JSONException |
public |
the single checked exception of the package | yes (json/JSONException.java:2) |
| 6 | JSON |
package-private | coercion helpers (toBoolean/toDouble/toInteger/toLong/toString), checkDouble, typeMismatch factories |
yes (json/JSON.java:2, :25) |
| 7 | JSONUtils |
public abstract … implements Utils |
the Microsphere-original bridge: StringBuilder JSON writer + JSON↔collections/beans reader |
no — ASF header, @since 1.0.0 (json/JSONUtils.java:2, :109-113) |
| 8 | package-info.java |
— | declares the fork: "Fork the source code from org.json" | — (json/package-info.java:19-22) |
JSONObject object = new JSONObject("{\"name\":\"Mercy\",\"age\":18}");
assertTrue("Mercy".equals(object.getString("name"))); // JSONException is checked — declare or catchCondensed from json/JSONObjectTest.java:110-114, which also proves the lenient = separator:
// json/JSONObjectTest.java:110-113
JSONObject jsonObject = new JSONObject("{ \"name\" : \"Mercy\" , \"age\" = 18 }");
assertEquals("Mercy", jsonObject.getString("name"));
assertEquals(18, jsonObject.getInt("age"));new JSONArray(String) is the mirror (json/JSONArray.java:140-142), proven at json/JSONArrayTest.java:98-104:
// json/JSONArrayTest.java:99-103
JSONArray jsonArray = new JSONArray("[ 1 ,2 , 3]");
assertEquals(3, jsonArray.length());
assertEquals(1, jsonArray.getInt(0));Both constructors delegate to JSONTokener and then steal the parsed internal map/list
(json/JSONObject.java:158-169, json/JSONArray.java:120-131). If the first value in the string is not the right
JSON type, you get a JSONException from JSON.typeMismatch (json/JSON.java:117-123) — asserted at
json/JSONArrayTest.java:92-95 ({} fed to new JSONArray(tokener)) and json/JSONObjectTest.java:105-107
(Hello fed to new JSONObject(tokener)).
Use the tokener when the input holds more than one value, or when you want the lexer's own methods:
// json/JSONTokenerTest.java:59-65 — the resource is microsphere-java-core/src/test/resources/test/json/data.json
JSONTokener jsonTokener = new JSONTokener(getResourceAsString("test/json/data.json"));
JSONObject jsonObject = (JSONObject) jsonTokener.nextValue();
assertEquals("Mercy", jsonObject.getString("name"));nextValue() returns JSONObject, JSONArray, String, Boolean, Integer, Long, Double,
JSONObject.NULL or an unquoted String — see the javadoc at json/JSONTokener.java:95-101 and the dispatch at
:102-122. Strings may be double- or single-quoted, and a bare word is accepted as a literal:
// json/JSONTokenerTest.java:76-88
assertEquals("Hello", new JSONTokener("\"Hello\"").nextValue());
assertEquals("hello", new JSONTokener("'hello'").nextValue());
assertEquals("Hello", new JSONTokener("Hello").nextValue()); // unquoted literal → StringA leading UTF-8 BOM is stripped once, in the constructor (json/JSONTokener.java:87-93);
json/JSONTokenerTest.java:43-46 constructs new JSONTokener("\ufeffHello").
readObject() and readArray() are public and assume the opening brace was already consumed — that is why they
look odd when called directly (json/JSONTokener.java:360-413, :415-461). The tests call them that way:
// json/JSONTokenerTest.java:92-98 — note: no leading "{"
JSONObject empty = new JSONTokener("}").readObject(); // length 0
JSONObject one = new JSONTokener("\"name\" : \"Mercy\" }").readObject();
assertEquals("Mercy", one.getString("name"));If you would rather not deal with a checked exception:
// json/JSONUtilsTest.java:508-512
JSONObject jsonObject = JSONUtils.jsonObject("{\"name\":\"John\", \"age\":30}");
assertEquals("John", jsonObject.getString("name"));
assertEquals(30, jsonObject.getInt("age"));
// json/JSONUtilsTest.java:521-526
JSONArray jsonArray = JSONUtils.jsonArray("[1,2,3]");
assertEquals(1, jsonArray.getInt(0));Malformed input is re-wrapped as an unchecked IllegalArgumentException whose cause is the original JSONException
(json/JSONUtils.java:803-812, :843-852, via util/ExceptionUtils.java:97-104 and :304-308):
// json/JSONUtilsTest.java:516-518 and :529-531
assertThrows(IllegalArgumentException.class, () -> jsonObject("{name}"));
assertThrows(IllegalArgumentException.class, () -> jsonArray("[1,2,3"));put returns this, so construction chains fluently. Every put overload also declares throws JSONException
(json/JSONObject.java:218-260), even the boolean/int/long ones, because they route through checkName
(:345-350).
// json/JSONObjectTest.java:123-136
long time = currentTimeMillis();
jsonObject.put("name", "Mercy")
.put("age", 18)
.put("workingTime", time)
.put("sex", true)
.put("salary", 1000.0)
.put("salary", null) // removes the mapping, see §10 item 1
.put("salary", Double.valueOf(1000.0));
assertEquals("Mercy", jsonObject.get("name"));
assertEquals(1000.0, jsonObject.getDouble("salary"));
assertTrue(jsonObject.getBoolean("sex"));putOpt is the null-tolerant variant — it silently no-ops when either argument is null
(json/JSONObject.java:296-301, proven at json/JSONObjectTest.java:140-145).
accumulate turns a name into an array one value at a time; the second value for the same name promotes the first
into a JSONArray (json/JSONObject.java:317-338):
// json/JSONObjectTest.java:155-170
jsonObject.accumulate("alias", "mercyblitz");
jsonObject.accumulate("alias", "mz");
assertEquals("mercyblitz", jsonObject.getJSONArray("alias").get(0));
assertEquals("mz", jsonObject.getJSONArray("alias").get(1));Arrays append by index too, null-padding on the way
(json/JSONArray.java:299-310):
// json/JSONArrayTest.java:143-148
jsonArray.put(0, true).put(1, 1).put(2, 2L).put(3, 3.0).put(4, "4");
assertEquals(3.0, jsonArray.get(3));remove returns the displaced value or null, and never throws
(json/JSONObject.java:359-361, json/JSONArray.java:366-371; tests at json/JSONObjectTest.java:179-184,
json/JSONArrayTest.java:187-198). isNull and has are the two membership probes
(json/JSONObject.java:370-373, :382-384; json/JSONArray.java:319-322; tests json/JSONObjectTest.java:187-203,
json/JSONArrayTest.java:152-158).
JSONObject and JSONArray expose identical getter families; only the key type differs (String name vs int
index). The coercion itself lives in the package-private JSON helper.
| Getter | Accepts | On failure | JSONObject lines |
JSONArray lines |
|---|---|---|---|---|
get / opt |
anything | get throws JSONException, opt returns null |
:393-399 / :407-409 |
:333-343 / :352-357 |
getBoolean / optBoolean |
Boolean, or the strings "true"/"false" case-insensitively |
throw / false or fallback |
:420-427 / :436-452 |
:382-389 / :398-414 |
getDouble / optDouble |
any Number, or a parseable String |
throw / NaN or fallback |
:463-470 / :479-495 |
:425-432 / :441-457 |
getInt / optInt |
any Number (narrowed), or a parseable String |
throw / 0 or fallback |
:505-512 / :521-537 |
:468-475 / :484-500 |
getLong / optLong |
any Number (narrowed), or a parseable String |
throw / 0L or fallback |
:548-555 / :566-584 |
:511-518 / :527-543 |
getString / optString |
String, or anything — non-null values go through String.valueOf |
throw / "" or fallback |
:593-600 / :609-625 |
:552-559 / :568-584 |
getJSONArray / optJSONArray |
JSONArray only |
throw / null |
:636-643 / :652-655 |
:595-602 / :611-614 |
getJSONObject / optJSONObject |
JSONObject only |
throw / null |
:666-673 / :682-685 |
:625-632 / :641-644 |
Every one of those cells is asserted in the suite — e.g. json/JSONObjectTest.java:307-339 for the no-fallback
opt forms and :544-577 for the explicit-fallback forms; json/JSONArrayTest.java:463-509 for the array side.
Numbers do not stay in the type you put in. JSON.toDouble / toInteger / toLong accept any Number and any
parseable String (json/JSON.java:50-96), so:
// json/JSONArrayTest.java:226-232, 255-261, 285-291
jsonArray.put(1.0); jsonArray.put("2.0"); jsonArray.put(3);
assertEquals(2.0, jsonArray.getDouble(1)); // String → double
assertEquals(2, jsonArray.getInt(1)); // String → double → int
assertEquals(3.0, jsonArray.getDouble(2)); // int → double
assertEquals(3L, jsonArray.getLong(2)); // double → long (lossy; javadoc says so at json/JSONObject.java:540-544)and a non-numeric string simply fails:
// json/JSONObjectTest.java:229-233, 242-246
jsonObject.put("value", "Hello");
assertThrows(JSONException.class, () -> jsonObject.getDouble("value"));
assertThrows(JSONException.class, () -> jsonObject.getInt("value"));getString is the one getter that coerces everything, because JSON.toString falls through to
String.valueOf(value) for any non-String (json/JSON.java:98-106):
// json/JSONTest.java:99-103
assertEquals("1", JSON.toString("1"));
assertEquals("1", JSON.toString(Integer.valueOf(1))); // number → its text
assertNull(JSON.toString(null));Note
The pretty-printing parameter is named indentSpaces, not indentFactor — see
json/JSONObject.java:758-762 and json/JSONArray.java:706-713. It is a literal count of space characters per
nesting level; there is no tab mode and no per-key ordering option.
// json/JSONObjectTest.java:419-420
assertEquals("{\"name\":\"Mercy\",\"age\":18,\"workingTime\":" + time + "}", jsonObject.toString());
assertEquals("{\n \"name\": \"Mercy\",\n \"age\": 18,\n \"workingTime\": " + time + "\n}",
jsonObject.toString(1));
// json/JSONArrayTest.java:414-415
assertEquals("[1,2,3]", jsonArray.toString());
assertEquals("[\n 1,\n 2,\n 3\n]", jsonArray.toString(1));Note the exact shape with indentSpaces = 1: a newline plus one space before each member, and ": " between key
and value (json/JSONStringer.java:388-397, :451-453). The closing bracket is never indented. Indentation comes
from a pre-built space string per instance (json/JSONStringer.java:136-144) repeated once per stack depth
(:394-396).
toString(0) — and therefore toString() — is the compact form; toString(negative) is also compact, because the
indent field stays null for anything <= 0 (json/JSONStringer.java:137-143).
JSONStringer is public but mostly an implementation detail (json/JSONStringer.java:33-35). Its value dispatch is
worth reading because it defines what every non-scalar becomes:
// json/JSONStringerTest.java:136-161 — same nine values, indented and compact
"[null,true,null,1,2,3,4,\"5\",\"java.lang.String\"]"Float 4.0f prints as 4, Double 2.0 prints as 2 (json/JSONObject.java:816-819), a StringBuilder prints as
a quoted toString() (json/JSONStringer.java:282-284), and a Class prints as its type name
(:280-281).
The three remaining public encoders:
// json/JSONObjectTest.java:437-442
assertEquals("0", numberToString(Double.valueOf(0.0))); // integral double → no ".0"
assertEquals("-0", numberToString(NEGATIVE_ZERO)); // json/JSONObject.java:811-814
assertEquals("1.23", numberToString(Double.valueOf(1.23)));
// json/JSONObjectTest.java:457-464
assertEquals("\"Mercy\"", quote("Mercy"));
assertEquals("\"\"", quote(null)); // json/JSONObject.java:832-834
// json/JSONArrayTest.java:402-407
jsonArray.put(1).put(2).put(3);
assertEquals("1,2,3", jsonArray.join(",")); // json/JSONArray.java:679-690quote and join both run through JSONStringer's Scope.NULL bracketless mode
(json/JSONObject.java:835-841, json/JSONArray.java:680-689) so they emit bare values with no {}/[].
JSONUtils writes JSON straight into your StringBuilder with no object model in between. It has 34 append
overloads — one per primitive, wrapper, String[], primitive array, object array and Object
(json/JSONUtils.java:146-467) — plus appendName (:500-508) and appendValue (:469-486). This is what
metadata uses (metadata/DefaultConfigurationPropertyGenerator.java:88-117).
// json/JSONUtilsTest.java:121-124, 230-232, 205-208
StringBuilder builder = new StringBuilder();
append(builder, "name", 1); // "name":1
append(builder, "name", "a"); // "name":"a" (escaped via JSONUtils.escape)
append(builder, "name", String.class); // "name":"java.lang.String"appendValue dispatches on runtime type: array → [...], Map → {...}, Iterable → [...],
Character/CharSequence/Enum → quoted, Type → quoted type name, everything else → StringBuilder.append(value)
(json/JSONUtils.java:469-486, proven by json/JSONUtilsTest.java:312-369).
Warning
JSONUtils.append and JSONObject.toString() are two different encoders with different escaping and different
validation. append performs no finiteness check and no name escaping; toString() performs both. Do not mix
them in one document and expect the bytes to agree. §10 items 4, 6 and 13 give the exact divergences.
This is JSONUtils, and it is the reason io.microsphere.json is more than a fork.
// json/JSONUtilsTest.java:663-675
String json = writeValueAsString(ofMap("key1", "value1", "key2", 2));
assertEquals(new JSONObject(map).toString(), json);
Object array = ofArray("value1", "value2");
assertEquals(new JSONArray(array).toString(), writeValueAsString(array));writeValueAsString delegates to JSONObject.wrap and returns null when the wrap did not produce a JSON
container (json/JSONUtils.java:1234-1241) — so writeValueAsString("Hello") is null, not "\"Hello\""
(json/JSONUtilsTest.java:678-681).
wrap's ladder is the whole conversion contract (json/JSONObject.java:859-886), and the suite pins every rung:
// json/JSONObjectTest.java:467-515 (selected)
assertEquals(NULL, wrap(null)); // :469
assertEquals(new JSONArray(ofList(values)), wrap(ofList(values))); // :479 Iterable
assertEquals(new JSONArray(ofArray(values)), wrap(ofArray(values))); // :481 array
assertEquals(new JSONArray(ofEnumeration(values)), wrap(ofEnumeration(values))); // :483
assertEquals(new JSONObject(ofMap("name", "Mercy")), wrap(ofMap("name", "Mercy"))); // :485
assertEquals("java.lang.String", wrap(String.class)); // :507
assertEquals("NEW", wrap(NEW)); // :509 enum → toString()
assertEquals(date.toString(), wrap(date)); // :512 any java.* → toString()
assertEquals(new JSONObject(), wrap(this)); // :514 JavaBean → propertiesA bean goes through BeanUtils.resolvePropertiesAsMap (json/JSONObject.java:881), and writeBeanAsString is the
direct route for that (json/JSONUtils.java:1272-1277, proven at json/JSONUtilsTest.java:689-694).
// json/JSONUtilsTest.java:594-599 (List)
List<Data> dataList = ofList(createData());
String json = writeValueAsString(dataList);
List<Data> copy = readValues(json, List.class, Data.class);
assertEquals(dataList, copy);readValues(String, Class, Class) supports exactly six target shapes and returns null for anything else
(json/JSONUtils.java:1125-1141): array (isArray), List, Set, Queue, Enumeration, Iterable
(otherwise). The null fallback is asserted at json/JSONUtilsTest.java:639-645:
Object dataCopy = readValues(json, Object.class, Data.class); // unsupported → null, no exception
assertNull(dataCopy);The other entry points:
// json/JSONUtilsTest.java:647-654 (typed array from a JSON string)
Data[] dataArrayCopy = readArray(json, Data.class);
assertArrayEquals(dataArray, dataArrayCopy);
// json/JSONUtilsTest.java:534-548 (JSONObject → Map, values converted where possible)
Map<String, Object> readValue = readValue(json, Map.class);
assertEquals(data.getName(), readValue.get("name"));
assertArrayEquals(data.getNames(), (String[]) readValue.get("names"));readValue(json, Map.class) reaches readValueAsMap because of the isAssignableFrom(Map.class, targetType)
branch (json/JSONUtils.java:917-922, :948-957). There is a Type-aware overload too —
readValues(JSONArray, Type) (json/JSONUtils.java:1073-1094) — which infers the element class from the content
when the target is not parameterised, via determineElementClass (:1302-1322).
// json/JSONUtilsTest.java:551-557
Data data = createData();
String json = writeValueAsString(data);
Data readValue = readValue(json, Data.class); // Data has a no-arg constructor + setters
assertEquals(data, readValue);and the multi-value bean, which is the interesting case because each property lands in a different collection type:
// json/JSONUtilsTest.java:560-571
md.setStringList(ofList("a", "b", "c"));
md.setIntegerSet(ofSet(1, 2, 3));
md.setDataQueue(ofQueue(createData()));
md.setClassEnumeration(ofEnumeration(String.class, Integer.class));
md.setObjects(ofArray("Hello", 123, true, 45.67));
MultipleValueData readValue = readValueAsBean(jsonObject(writeValueAsString(md)), MultipleValueData.class);
assertEquals(md, readValue);The mechanism is: instantiate with ConstructorUtils.newInstance(true, beanClass), then for every JSON key find a
write method and convert the value to the generic parameter type
(json/JSONUtils.java:988-1007). Anything it cannot match is skipped quietly — see
json/JSONUtilsTest.java:573-581, where a null-valued key and an unrelated "test-name" key both disappear.
Tip
Round-tripping a char bean property through JSON loses its type: Character is a wrapper so wrap returns it
as-is (json/JSONObject.java:864), the writer quotes it (json/JSONStringer.java:282-284), and it comes back as a
one-character String. The suite encodes exactly that at json/JSONUtilsTest.java:546
(String.valueOf(data.getSex()) compared against the read value). Likewise short/byte come back as Integer
in the Map path (:544-545), and float as Double (:542).
-
There are two nulls and they are not interchangeable.
JSONObject.NULLis a sentinel whoseequalsdeliberately returnstruefornull(json/JSONObject.java:101-114, asserted atjson/JSONObjectTest.java:525-529), while the Javanullreference is not a legal map value. Consequences:put(name, null)removes the key (json/JSONObject.java:275-278), sonew JSONObject().put("a", null)serialises as{}whilenew JSONObject("{\"a\":null}")serialises as{"a":null}andhas("a")istrue(:382-384) butisNull("a")is alsotrue(:370-373,json/JSONObjectTest.java:187-195).JSONArray.get(index)throws on a rawnullelement yet succeeds onNULL(json/JSONArray.java:333-343, warned at:49-52, asserted atjson/JSONArrayTest.java:167-170). UseJSONUtils.isNull(Object)(json/JSONUtils.java:697-699) if you want one predicate for both. -
getXis not uniform about which failure it reports.getInt,getLong,getDouble,getJSONArrayandgetJSONObjectall start fromget(name), which throws"No value for <name>"for a missing key (json/JSONObject.java:393-399, used at:463,:505,:548,:636,:666).getBooleanandgetStringstart fromopt(name)instead (:421,:594), so the same missing key produces"Value at <name> is null."fromJSON.typeMismatch(json/JSON.java:108-115). If you match on the message, you will miss half the cases. -
JSONObject.getStringreports the wrong target type. Atjson/JSONObject.java:597it callstypeMismatch(name, object, "long")— a copy-paste defect;json/JSONArray.java:556(its twin) correctly passes"String". The thrown exception is the right class, only the message says "cannot be converted to long". -
NaN/Infinityare rejected on one write path and written verbatim on the other. The model path refuses them:JSON.checkDouble(json/JSON.java:27-32) is called fromput(String,double)(json/JSONObject.java:232-234),put(String,Object)for anyNumber(:279-283),accumulate(:324-326),JSONArray.put(double)(json/JSONArray.java:189-192) andput(int,Object)(:299-304); tests atjson/JSONObjectTest.java:537-541andjson/JSONArrayTest.java:450-461.JSONUtils.appenddoes none of this —append(builder, name, 1.0)goes straight toStringBuilder.append(double)(json/JSONUtils.java:176-179), andappend(builder, name, (Object) Double.NaN)falls to theelsebranch ofappendValue(:483-485). Both produceNaN/Infinity, which is not valid JSON, and no exception is raised. Compounding it,JSON.toDoublehappily parses"NaN","Infinity"and"-Infinity"because it callsDouble.valueOf(String)(json/JSON.java:50-64). -
JSONArray.put(Object)is unguarded, and the failure surfaces later as an unchecked exception. The overload atjson/JSONArray.java:225-228does noNumbercheck;json/JSONObject.java:323-324even comments the reason ("check in accumulate, since array.put(Object) doesn't do any checking"). Sonew JSONArray().put((Object) NaN)succeeds andtoString()afterwards throwsRuntimeExceptionwrappingJSONException(json/JSONArray.java:713-717→lang/function/ThrowableAction.java:79-81). No test covers this shape —json/JSONArrayTest.java:450-461only exercises the checkedput(double)andput(int,double)overloads. -
Two encoders, two escaping rules.
JSONStringer.string()escapes",\,/, the six named escapes and everything<= 0x1F(json/JSONStringer.java:338-386;json/JSONStringerTest.java:188-194pins"\"\\\//\t\b\n\r\fa"→"\"\\\\\\/\\t\\b\\n\\r\\fa"and U+001F →\u001f).JSONUtils.escape()escapes",\and the control range, not/, but does escapeU+2028/U+2029(json/JSONUtils.java:115-144,:1342-1386;json/JSONUtilsTest.java:806-815). SoJSONObject.toString()yields\/whereJSONUtils.appendyields/, and the reverse for U+2028. -
JSONUtilsnever escapes property names, and never escapescharvalues.appendNamewrites'"' + name + '"' + ':'with no escaping (json/JSONUtils.java:500-508), andappend(builder, name, 'a')/append(builder, name, Character.valueOf('a'))write the character raw (:181-186,:223-228). A name containing"or acharvalue of"produces malformed JSON silently. The tests only use benign names (json/JSONUtilsTest.java:372-384,:144-148,:192-196). -
A malformed
\uXXXXescape throwsNumberFormatException, notJSONException.readEscapeCharacterparses the four hex digits withInteger.parseInt(hex, 16)and catches nothing (json/JSONTokener.java:254-260); the javadoc acknowledges it at:200and:248."\uZZZZ"therefore escapes acatch (JSONException). The suite only covers the truncated variant,"u123", which hits the length check and does throwJSONException(json/JSONTokenerTest.java:223-227). -
There is no nesting limit on either side, and no cycle guard. The parser recurses
nextValue()→readObject()/readArray()→nextValue()(json/JSONTokener.java:102-122,:367-413,:423-461) with no counter, and the writer recursesJSONStringer.value(Object)→JSONObject.writeTo(stringer)→stringer.value(...)(json/JSONStringer.java:266-271,json/JSONObject.java:768-774). Deeply nested input therefore dies withStackOverflowError, anErrorthat nocatch (JSONException | RuntimeException)will intercept.JSONStringer's class javadoc still claims "at most 20 levels of nesting" (json/JSONStringer.java:61-62) while the field comment right below says the opposite — "Unlike the original implementation, this stack isn't limited to 20 levels of nesting" (:120-123). Cycles are also constructible by hand (json/JSONArrayTest.java:338-341puts an array inside itself) andtoString()on such an array overflows. -
Numbers never become
BigDecimalorBigInteger.readLiteraltriesInteger, thenLong, thenDoubleand falls back to an unquotedString(json/JSONTokener.java:305-339); the whole package contains zero references toBigDecimal/BigInteger.21474836470parses to aLong(json/JSONTokenerTest.java:241) but anything beyondLong.MAX_VALUEsilently becomes aDoubleand loses precision.JSON.toIntegerandtoLongnarrow with a C-style cast (json/JSON.java:75,:91), sogetInt("999999999999")returnsInteger.MAX_VALUErather than failing —JSONTest.java:84already shows"1.0"becoming1. -
jsonObject(null)/jsonArray(null)throwNullPointerException, not the documentedIllegalArgumentException.new JSONTokener(null)accepts thenulland stores it (json/JSONTokener.java:87-93, with the javadoc admitting it at:84-85); the first read dereferences it at:501-503. SinceJSONUtils.jsonObjectonly catchesJSONException(json/JSONUtils.java:806-810), the NPE escapes unwrapped and the@throws IllegalArgumentExceptioncontract at:801is not met. There is no test for thenullinput —json/JSONTokenerTest.java:49-51constructs the tokener withnulland never reads from it. -
wrapandreadValuesswallow failures intonull.JSONObject.wrapwraps its entire dispatch incatch (Exception ignored) { }(json/JSONObject.java:867-885), so a map with anullkey, a bean whose getter throws, or a self-equals-broken value all end up asnull— asserted atjson/JSONObjectTest.java:518-522and then re-serialized away byJSONUtils.writeValueAsString(json/JSONUtils.java:1235-1241,json/JSONUtilsTest.java:678-687).readValuessimilarly returnsnullfor an unsupported target type (:1125-1141,json/JSONUtilsTest.java:639-645). Neither is a bug, but both mean anullresult tells you nothing about why. -
JSONUtils' null handling is the opposite of the model's.appendValuecallsvalue.getClass()as its first statement (json/JSONUtils.java:469-470), soappend(builder, "k", (Object) null)throwsNullPointerException— and so does anyMaporIterablecontaining onenullelement, viaappendMap/appendIterable(:1388-1415). The same holds fordetermineElementClass, which dereferences every element (:1303-1310), and forreadValueAsMap/readValueAsBean, which calljsonObject.length()/getBeanMetadata(...)before any null check (:948-957,:988-990). No test injson/JSONUtilsTest.java:319-334asserts anything about the map case — that test has no assertion at all.
- Configuration property metadata —
metadatais the largest in-library consumer:DefaultConfigurationPropertyGeneratorwrites JSON withJSONUtils.append(metadata/DefaultConfigurationPropertyGenerator.java:29-30,88-117),ReflectiveConfigurationPropertyGeneratorwithwriteBeanAsString(metadata/ReflectiveConfigurationPropertyGenerator.java:24,78), andDefaultConfigurationPropertyReaderreads withreadValues(content, List.class, Map.class)(metadata/DefaultConfigurationPropertyReader.java:29,43). - Reflection, types and beans —
BeanUtils.resolvePropertiesAsMapis what makesJSONObject.wraphandle JavaBeans (json/JSONObject.java:25,881), andConstructorUtils/MethodUtils/TypeUtilsdrivereadValueAsBean(json/JSONUtils.java:988-1007). - Type conversion —
Converter.convertIfPossibleis the leaf of every JSON→Java scalar conversion (json/JSONUtils.java:63,1427-1442). - Collections and filters —
ListUtils.newArrayListandMapUtils.newLinkedHashMapdecideJSONArray's storage andJSONObject's key ordering (json/JSONArray.java:84,json/JSONObject.java:122). - Utilities and constants —
ClassUtilssupplies every type predicate used bywrap(json/JSONObject.java:39-45) andSymbolConstantssupplies the punctuation used byJSONUtils(json/JSONUtils.java:56-62). - Serialization — the neighbouring
io.microsphere.io.serializerpackage, which covers binaryObject↔byte[]and is the right choice when you do not want a text format. - Developer Guide — JSON — the tokener state machine, the
JSONObjectmap choice, the writer's scope stack, the exception taxonomy, the design-pattern inventory, the package-dependency graph and the review notes.