Skip to content

Commit 32967d5

Browse files
authored
fix Issue 177 (support incomplet json messages) (#254)
* Add support for incomplete JSON parsing and enhance documentation - Introduced ACCEPT_INCOMPLETE mode in JSONParser to allow parsing of incomplete JSON data. - Updated JSONParserBase and related classes to handle incomplete data scenarios. - Added unit tests for various incomplete JSON cases in JSONIncompletModeTest. * Enhance test coverage for JSONArray, JSONObject, JSONUtil, and JSONValue * Add JSONParser.ACCEPT_INCOMPLETE to support parsing of partial JSON in changelog * add a precommit script * RENAME acceptIncomplet to acceptIncomplete * rename MODE_PERMISSIVE_NEW to MODE_PERMISSIVE_WITH_INCOMPLETE * improve JSDoc syntax
1 parent bf0f659 commit 32967d5

15 files changed

+571
-18
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ So I do not use my json-smart anymore. I had fun with this project. If you want
2222
### *V 2.6.0* (next version)
2323

2424
* JSONObject merge support overwrite as parameter. [PR 238](https://github.com/netplex/json-smart-v2/pull/238)
25+
* Add `JSONParser.ACCEPT_INCOMPLETE` to allow parsing partial and incomplete JSON without error [PR 254](https://github.com/netplex/json-smart-v2/pull/254)
2526

2627
### *V 2.5.2* (2025-02-12)
2728

json-smart/src/main/java/net/minidev/json/JSONArray.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626
* @author FangYidong <[email protected]>
2727
* @author Uriel Chemouni <[email protected]>
2828
*/
29-
public class JSONArray extends ArrayList<Object>
30-
implements List<Object>, JSONAwareEx, JSONStreamAwareEx {
29+
public class JSONArray extends ArrayList<Object> implements JSONAwareEx, JSONStreamAwareEx {
3130
private static final long serialVersionUID = 9106884089231309568L;
3231

3332
public JSONArray() {}
@@ -76,13 +75,21 @@ public static void writeJSONString(
7675
JsonWriter.JSONIterableWriter.writeJSONString(list, out, compression);
7776
}
7877

78+
/**
79+
* Encode a list into JSON text and write it to out. If this list is also a JSONStreamAware or a
80+
* JSONAware, JSONStreamAware and JSONAware specific behaviours will be ignored at this top level.
81+
*
82+
* @param list
83+
* @param out
84+
* @throws IOException
85+
*/
7986
public static void writeJSONString(List<? extends Object> list, Appendable out)
8087
throws IOException {
8188
writeJSONString(list, out, JSONValue.COMPRESSION);
8289
}
8390

8491
/**
85-
* Appends the specified element and returns this. Handy alternative to add(E e) method.
92+
* Appends the specified element and returns this. same effect that add(E e) method.
8693
*
8794
* @param element element to be appended to this array.
8895
* @return this
@@ -92,6 +99,11 @@ public JSONArray appendElement(Object element) {
9299
return this;
93100
}
94101

102+
/**
103+
* Merges the specified object into this array. can trigger an add(E e) or addAll(E e) method.
104+
*
105+
* @param o2
106+
*/
95107
public void merge(Object o2) {
96108
JSONObject.merge(this, o2);
97109
}
@@ -119,10 +131,21 @@ public String toString(JSONStyle compression) {
119131
return toJSONString(compression);
120132
}
121133

134+
/**
135+
* JSONStreamAwareEx interface
136+
*
137+
* @param out output stream
138+
*/
122139
public void writeJSONString(Appendable out) throws IOException {
123140
writeJSONString(this, out, JSONValue.COMPRESSION);
124141
}
125142

143+
/**
144+
* JSONStreamAwareEx interface
145+
*
146+
* @param out output stream
147+
* @param compression compression param
148+
*/
126149
public void writeJSONString(Appendable out, JSONStyle compression) throws IOException {
127150
writeJSONString(this, out, compression);
128151
}

json-smart/src/main/java/net/minidev/json/JSONUtil.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ else if (dest == double.class)
4545
if (obj instanceof Number) return ((Number) obj).doubleValue();
4646
else return Double.valueOf(obj.toString());
4747
else if (dest == char.class) {
48-
String asString = dest.toString();
48+
String asString = obj.toString();
4949
if (asString.length() > 0) return Character.valueOf(asString.charAt(0));
5050
} else if (dest == boolean.class) {
51-
return (Boolean) obj;
51+
if (obj instanceof Number) return ((Number) obj).intValue() != 0;
52+
if (obj instanceof String) return Boolean.valueOf((String) obj);
5253
}
5354
throw new RuntimeException(
5455
"Primitive: Can not convert " + obj.getClass().getName() + " to " + dest.getName());
@@ -73,9 +74,13 @@ else if (dest == char.class) {
7374
if (obj instanceof Number) return Double.valueOf(((Number) obj).doubleValue());
7475
else return Double.valueOf(obj.toString());
7576
if (dest == Character.class) {
76-
String asString = dest.toString();
77+
String asString = obj.toString();
7778
if (asString.length() > 0) return Character.valueOf(asString.charAt(0));
7879
}
80+
if (dest == Boolean.class) {
81+
if (obj instanceof Number) return ((Number) obj).intValue() != 0;
82+
if (obj instanceof String) return Boolean.valueOf((String) obj);
83+
}
7984
throw new RuntimeException(
8085
"Object: Can not Convert " + obj.getClass().getName() + " to " + dest.getName());
8186
}
@@ -94,10 +99,11 @@ public static Object convertToX(Object obj, Class<?> dest) {
9499
else if (dest == float.class) return Float.valueOf(obj.toString());
95100
else if (dest == double.class) return Double.valueOf(obj.toString());
96101
else if (dest == char.class) {
97-
String asString = dest.toString();
102+
String asString = obj.toString();
98103
if (asString.length() > 0) return Character.valueOf(asString.charAt(0));
99104
} else if (dest == boolean.class) {
100-
return (Boolean) obj;
105+
if (obj instanceof Number) return ((Number) obj).intValue() != 0;
106+
if (obj instanceof String) return Boolean.valueOf((String) obj);
101107
}
102108
throw new RuntimeException(
103109
"Primitive: Can not convert " + obj.getClass().getName() + " to " + dest.getName());
@@ -122,9 +128,13 @@ else if (dest == char.class) {
122128
if (obj instanceof Number) return Double.valueOf(((Number) obj).doubleValue());
123129
else return Double.valueOf(obj.toString());
124130
if (dest == Character.class) {
125-
String asString = dest.toString();
131+
String asString = obj.toString();
126132
if (asString.length() > 0) return Character.valueOf(asString.charAt(0));
127133
}
134+
if (dest == Boolean.class) {
135+
if (obj instanceof Number) return ((Number) obj).intValue() != 0;
136+
if (obj instanceof String) return Boolean.valueOf((String) obj);
137+
}
128138
throw new RuntimeException(
129139
"Object: Can not Convert " + obj.getClass().getName() + " to " + dest.getName());
130140
}

json-smart/src/main/java/net/minidev/json/parser/JSONParser.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,36 @@ public class JSONParser {
9494
public static final int BIG_DIGIT_UNRESTRICTED = 2048;
9595

9696
/**
97-
* If limit the max depth of json size
97+
* If limit the max depth of JSON size
9898
*
9999
* @since 2.5
100100
*/
101101
public static final int LIMIT_JSON_DEPTH = 4096;
102102

103103
/**
104-
* smart mode, fastest parsing mode. accept lots of non standard json syntax
104+
* If the parser is in stream mode
105+
*
106+
* <p>Stream mode is used to parse a stream of JSON data. It will not throw exception on
107+
* incomplete data.
108+
*
109+
* @since 2.6
110+
*/
111+
public static final int ACCEPT_INCOMPLETE = 8192;
112+
113+
/**
114+
* smart mode, fastest parsing mode. accept lots of non standard JSON syntax ACCEPT_INCOMPLETE
115+
* feature is not enabled. in this mode, for backward compatibility
105116
*
106117
* @since 1.0.6
107118
*/
108-
public static final int MODE_PERMISSIVE = -1;
119+
public static final int MODE_PERMISSIVE = -1 & ~ACCEPT_INCOMPLETE;
120+
121+
/*
122+
* smart mode, fastest parsing mode. accept lots of non standard JSON syntax
123+
* ACCEPT_INCOMPLETE feature is enabled.
124+
* @since 2.6
125+
*/
126+
public static final int MODE_PERMISSIVE_WITH_INCOMPLETE = -1;
109127

110128
/**
111129
* strict RFC4627 mode.

json-smart/src/main/java/net/minidev/json/parser/JSONParserBase.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ abstract class JSONParserBase {
8989
protected final boolean reject127;
9090
protected final boolean unrestictBigDigit;
9191
protected final boolean limitJsonDepth;
92+
protected final boolean acceptIncomplete;
9293

9394
public JSONParserBase(int permissiveMode) {
9495
this.acceptNaN = (permissiveMode & JSONParser.ACCEPT_NAN) > 0;
@@ -106,6 +107,7 @@ public JSONParserBase(int permissiveMode) {
106107
this.reject127 = (permissiveMode & JSONParser.REJECT_127_CHAR) > 0;
107108
this.unrestictBigDigit = (permissiveMode & JSONParser.BIG_DIGIT_UNRESTRICTED) > 0;
108109
this.limitJsonDepth = (permissiveMode & JSONParser.LIMIT_JSON_DEPTH) > 0;
110+
this.acceptIncomplete = (permissiveMode & JSONParser.ACCEPT_INCOMPLETE) > 0;
109111
}
110112

111113
public void checkControleChar() throws ParseException {
@@ -313,6 +315,10 @@ protected <T> T readArray(JsonReaderI<T> mapper) throws ParseException, IOExcept
313315
needData = true;
314316
continue;
315317
case EOI:
318+
if (acceptIncomplete) {
319+
this.depth--;
320+
return mapper.convert(current);
321+
}
316322
throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, "EOF");
317323
default:
318324
mapper.addValue(current, readMain(mapper, stopArray));
@@ -491,6 +497,11 @@ protected Object readMain(JsonReaderI<?> mapper, boolean stop[])
491497
if (!acceptNonQuote) throw new ParseException(pos, ERROR_UNEXPECTED_TOKEN, xs);
492498
//
493499
return xs;
500+
case EOI:
501+
if (acceptIncomplete) {
502+
return null;
503+
}
504+
throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, "EOF");
494505
// digits
495506
case '0':
496507
case '1':
@@ -527,6 +538,7 @@ protected <T> T readObject(JsonReaderI<T> mapper) throws ParseException, IOExcep
527538
if (limitJsonDepth && ++this.depth > MAX_DEPTH) {
528539
throw new ParseException(pos, ERROR_UNEXPECTED_JSON_DEPTH, c);
529540
}
541+
// Store the appropriate read method in a variable
530542
Object current = mapper.createObject();
531543
boolean needData = false;
532544
boolean acceptData = true;
@@ -572,7 +584,14 @@ protected <T> T readObject(JsonReaderI<T> mapper) throws ParseException, IOExcep
572584
skipSpace();
573585

574586
if (c != ':') {
575-
if (c == EOI) throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, null);
587+
if (c == EOI) {
588+
if (acceptIncomplete) {
589+
this.depth--;
590+
mapper.setValue(current, key, null);
591+
return mapper.convert(current);
592+
}
593+
throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, null);
594+
}
576595
throw new ParseException(pos - 1, ERROR_UNEXPECTED_CHAR, c);
577596
}
578597
readNoEnd(); /* skip : */
@@ -593,8 +612,13 @@ protected <T> T readObject(JsonReaderI<T> mapper) throws ParseException, IOExcep
593612
//
594613
return mapper.convert(current);
595614
}
596-
if (c == EOI) // Fixed on 18/10/2011 reported by vladimir
597-
throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, null);
615+
if (c == EOI) { // Fixed on 18/10/2011 reported by vladimir
616+
if (acceptIncomplete) {
617+
this.depth--;
618+
return mapper.convert(current);
619+
}
620+
throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, null);
621+
}
598622
// if c==, continue
599623
if (c == ',') acceptData = needData = true;
600624
else throw new ParseException(pos - 1, ERROR_UNEXPECTED_TOKEN, c);
@@ -615,6 +639,10 @@ protected void readString2() throws ParseException, IOException {
615639
read();
616640
switch (c) {
617641
case EOI:
642+
if (acceptIncomplete) {
643+
xs = sb.toString();
644+
return;
645+
}
618646
throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, null);
619647
case '"':
620648
case '\'':

json-smart/src/main/java/net/minidev/json/parser/JSONParserByteArray.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ protected void readS() {
9696
protected void readNoEnd() throws ParseException {
9797
if (++pos >= len) {
9898
this.c = EOI;
99+
if (super.acceptIncomplete) return;
99100
throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, "EOF");
100101
} else this.c = (char) in[pos];
101102
}

json-smart/src/main/java/net/minidev/json/parser/JSONParserMemory.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,13 @@ protected void readString() throws ParseException, IOException {
113113
throw new ParseException(pos, ERROR_UNEXPECTED_CHAR, c);
114114
}
115115
int tmpP = indexOf(c, pos + 1);
116-
if (tmpP == -1) throw new ParseException(len, ERROR_UNEXPECTED_EOF, null);
116+
if (tmpP == -1) {
117+
if (acceptIncomplete) {
118+
readString2();
119+
return;
120+
}
121+
throw new ParseException(len, ERROR_UNEXPECTED_EOF, null);
122+
}
117123
extractString(pos + 1, tmpP);
118124
if (xs.indexOf('\\') == -1) {
119125
checkControleChar();

json-smart/src/main/java/net/minidev/json/parser/JSONParserReader.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,11 @@ protected void readS() throws IOException {
8282

8383
protected void readNoEnd() throws ParseException, IOException {
8484
int i = in.read();
85-
if (i == -1) throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, "EOF");
85+
if (i == -1) {
86+
this.c = EOI;
87+
if (super.acceptIncomplete) return;
88+
throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, "EOF");
89+
}
8690
c = (char) i;
87-
//
8891
}
8992
}

json-smart/src/main/java/net/minidev/json/parser/JSONParserString.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ protected void readS() {
9393
protected void readNoEnd() throws ParseException {
9494
if (++pos >= len) {
9595
this.c = EOI;
96+
if (super.acceptIncomplete) return;
9697
throw new ParseException(pos - 1, ERROR_UNEXPECTED_EOF, "EOF");
9798
} else this.c = in.charAt(pos);
9899
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package net.minidev.json.test;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import net.minidev.json.JSONArray;
6+
import net.minidev.json.JSONObject;
7+
import net.minidev.json.parser.JSONParser;
8+
import org.junit.jupiter.api.Test;
9+
10+
/** TODO make the same tests in stream and bytes mode */
11+
public class JSONIncompletModeTest {
12+
13+
@Test
14+
public void testArraySimple() throws Exception {
15+
String s = "[1";
16+
JSONParser p = new JSONParser(JSONParser.MODE_JSON_SIMPLE | JSONParser.ACCEPT_INCOMPLETE);
17+
JSONArray array = (JSONArray) p.parse(s);
18+
assertEquals(Long.valueOf(1), (Long) array.get(0));
19+
}
20+
21+
@Test
22+
public void testArrayInObject1() throws Exception {
23+
String s = "{\"obj\":[1";
24+
String result = "{\"obj\":[1]}";
25+
26+
JSONParser p = new JSONParser(JSONParser.MODE_JSON_SIMPLE | JSONParser.ACCEPT_INCOMPLETE);
27+
JSONObject array = (JSONObject) p.parse(s);
28+
assertEquals(result, array.toJSONString());
29+
}
30+
31+
@Test
32+
public void testObjectCut() throws Exception {
33+
String s = "{\"obj\":";
34+
String result = "{\"obj\":null}";
35+
36+
JSONParser p = new JSONParser(JSONParser.MODE_JSON_SIMPLE | JSONParser.ACCEPT_INCOMPLETE);
37+
JSONObject array = (JSONObject) p.parse(s);
38+
assertEquals(result, array.toJSONString());
39+
}
40+
41+
@Test
42+
public void testObjectCut2() throws Exception {
43+
String s = "{\"obj\"";
44+
String result = "{\"obj\":null}";
45+
46+
JSONParser p = new JSONParser(JSONParser.MODE_JSON_SIMPLE | JSONParser.ACCEPT_INCOMPLETE);
47+
JSONObject array = (JSONObject) p.parse(s);
48+
assertEquals(result, array.toJSONString());
49+
}
50+
51+
@Test
52+
public void testObjectCut3() throws Exception {
53+
String s = "{\"obj";
54+
String result = "{\"obj\":null}";
55+
56+
JSONParser p = new JSONParser(JSONParser.MODE_JSON_SIMPLE | JSONParser.ACCEPT_INCOMPLETE);
57+
JSONObject array = (JSONObject) p.parse(s);
58+
assertEquals(result, array.toJSONString());
59+
}
60+
61+
@Test
62+
public void testObjectCut4() throws Exception {
63+
String s = "{\"obj\":\"";
64+
String result = "{\"obj\":\"\"}";
65+
66+
JSONParser p = new JSONParser(JSONParser.MODE_JSON_SIMPLE | JSONParser.ACCEPT_INCOMPLETE);
67+
JSONObject array = (JSONObject) p.parse(s);
68+
assertEquals(result, array.toJSONString());
69+
}
70+
71+
@Test
72+
public void testStringCut() throws Exception {
73+
String s = "\"obj";
74+
String result = "obj";
75+
76+
JSONParser p = new JSONParser(JSONParser.MODE_JSON_SIMPLE | JSONParser.ACCEPT_INCOMPLETE);
77+
String array = (String) p.parse(s);
78+
assertEquals(result, array);
79+
}
80+
}

0 commit comments

Comments
 (0)