Skip to content

Commit 2e9ad6f

Browse files
authored
Merge pull request #966 from marilynel/master
granular flags to control for keeping boolean or number values as strings
2 parents 4917e35 + 8dbf03e commit 2e9ad6f

File tree

3 files changed

+204
-16
lines changed

3 files changed

+204
-16
lines changed

Diff for: src/main/java/org/json/XML.java

+98-6
Original file line numberDiff line numberDiff line change
@@ -355,10 +355,20 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
355355
&& TYPE_ATTR.equals(string)) {
356356
xmlXsiTypeConverter = config.getXsiTypeMap().get(token);
357357
} else if (!nilAttributeFound) {
358-
jsonObject.accumulate(string,
359-
config.isKeepStrings()
360-
? ((String) token)
361-
: stringToValue((String) token));
358+
Object obj = stringToValue((String) token);
359+
if (obj instanceof Boolean) {
360+
jsonObject.accumulate(string,
361+
config.isKeepBooleanAsString()
362+
? ((String) token)
363+
: obj);
364+
} else if (obj instanceof Number) {
365+
jsonObject.accumulate(string,
366+
config.isKeepNumberAsString()
367+
? ((String) token)
368+
: obj);
369+
} else {
370+
jsonObject.accumulate(string, stringToValue((String) token));
371+
}
362372
}
363373
token = null;
364374
} else {
@@ -407,8 +417,20 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
407417
jsonObject.accumulate(config.getcDataTagName(),
408418
stringToValue(string, xmlXsiTypeConverter));
409419
} else {
410-
jsonObject.accumulate(config.getcDataTagName(),
411-
config.isKeepStrings() ? string : stringToValue(string));
420+
Object obj = stringToValue((String) token);
421+
if (obj instanceof Boolean) {
422+
jsonObject.accumulate(config.getcDataTagName(),
423+
config.isKeepBooleanAsString()
424+
? ((String) token)
425+
: obj);
426+
} else if (obj instanceof Number) {
427+
jsonObject.accumulate(config.getcDataTagName(),
428+
config.isKeepNumberAsString()
429+
? ((String) token)
430+
: obj);
431+
} else {
432+
jsonObject.accumulate(config.getcDataTagName(), stringToValue((String) token));
433+
}
412434
}
413435
}
414436

@@ -688,6 +710,44 @@ public static JSONObject toJSONObject(Reader reader, boolean keepStrings) throws
688710
return toJSONObject(reader, XMLParserConfiguration.ORIGINAL);
689711
}
690712

713+
/**
714+
* Convert a well-formed (but not necessarily valid) XML into a
715+
* JSONObject. Some information may be lost in this transformation because
716+
* JSON is a data format and XML is a document format. XML uses elements,
717+
* attributes, and content text, while JSON uses unordered collections of
718+
* name/value pairs and arrays of values. JSON does not does not like to
719+
* distinguish between elements and attributes. Sequences of similar
720+
* elements are represented as JSONArrays. Content text may be placed in a
721+
* "content" member. Comments, prologs, DTDs, and <pre>{@code
722+
* &lt;[ [ ]]>}</pre>
723+
* are ignored.
724+
*
725+
* All numbers are converted as strings, for 1, 01, 29.0 will not be coerced to
726+
* numbers but will instead be the exact value as seen in the XML document depending
727+
* on how flag is set.
728+
* All booleans are converted as strings, for true, false will not be coerced to
729+
* booleans but will instead be the exact value as seen in the XML document depending
730+
* on how flag is set.
731+
*
732+
* @param reader The XML source reader.
733+
* @param keepNumberAsString If true, then numeric values will not be coerced into
734+
* numeric values and will instead be left as strings
735+
* @param keepBooleanAsString If true, then boolean values will not be coerced into
736+
* * numeric values and will instead be left as strings
737+
* @return A JSONObject containing the structured data from the XML string.
738+
* @throws JSONException Thrown if there is an errors while parsing the string
739+
*/
740+
public static JSONObject toJSONObject(Reader reader, boolean keepNumberAsString, boolean keepBooleanAsString) throws JSONException {
741+
XMLParserConfiguration xmlParserConfiguration = new XMLParserConfiguration();
742+
if(keepNumberAsString) {
743+
xmlParserConfiguration = xmlParserConfiguration.withKeepNumberAsString(keepNumberAsString);
744+
}
745+
if(keepBooleanAsString) {
746+
xmlParserConfiguration = xmlParserConfiguration.withKeepBooleanAsString(keepBooleanAsString);
747+
}
748+
return toJSONObject(reader, xmlParserConfiguration);
749+
}
750+
691751
/**
692752
* Convert a well-formed (but not necessarily valid) XML into a
693753
* JSONObject. Some information may be lost in this transformation because
@@ -746,6 +806,38 @@ public static JSONObject toJSONObject(String string, boolean keepStrings) throws
746806
return toJSONObject(new StringReader(string), keepStrings);
747807
}
748808

809+
/**
810+
* Convert a well-formed (but not necessarily valid) XML string into a
811+
* JSONObject. Some information may be lost in this transformation because
812+
* JSON is a data format and XML is a document format. XML uses elements,
813+
* attributes, and content text, while JSON uses unordered collections of
814+
* name/value pairs and arrays of values. JSON does not does not like to
815+
* distinguish between elements and attributes. Sequences of similar
816+
* elements are represented as JSONArrays. Content text may be placed in a
817+
* "content" member. Comments, prologs, DTDs, and <pre>{@code
818+
* &lt;[ [ ]]>}</pre>
819+
* are ignored.
820+
*
821+
* All numbers are converted as strings, for 1, 01, 29.0 will not be coerced to
822+
* numbers but will instead be the exact value as seen in the XML document depending
823+
* on how flag is set.
824+
* All booleans are converted as strings, for true, false will not be coerced to
825+
* booleans but will instead be the exact value as seen in the XML document depending
826+
* on how flag is set.
827+
*
828+
* @param string
829+
* The source string.
830+
* @param keepNumberAsString If true, then numeric values will not be coerced into
831+
* numeric values and will instead be left as strings
832+
* @param keepBooleanAsString If true, then boolean values will not be coerced into
833+
* numeric values and will instead be left as strings
834+
* @return A JSONObject containing the structured data from the XML string.
835+
* @throws JSONException Thrown if there is an errors while parsing the string
836+
*/
837+
public static JSONObject toJSONObject(String string, boolean keepNumberAsString, boolean keepBooleanAsString) throws JSONException {
838+
return toJSONObject(new StringReader(string), keepNumberAsString, keepBooleanAsString);
839+
}
840+
749841
/**
750842
* Convert a well-formed (but not necessarily valid) XML string into a
751843
* JSONObject. Some information may be lost in this transformation because

Diff for: src/main/java/org/json/XMLParserConfiguration.java

+74-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ public class XMLParserConfiguration extends ParserConfiguration {
2222
*/
2323
// public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512; // We could override
2424

25+
/**
26+
* Allow user to control how numbers are parsed
27+
*/
28+
private boolean keepNumberAsString;
29+
30+
/**
31+
* Allow user to control how booleans are parsed
32+
*/
33+
private boolean keepBooleanAsString;
34+
2535
/** Original Configuration of the XML Parser. */
2636
public static final XMLParserConfiguration ORIGINAL
2737
= new XMLParserConfiguration();
@@ -142,7 +152,9 @@ public XMLParserConfiguration (final boolean keepStrings, final String cDataTagN
142152
*/
143153
@Deprecated
144154
public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, final boolean convertNilAttributeToNull) {
145-
super(keepStrings, DEFAULT_MAXIMUM_NESTING_DEPTH);
155+
super(false, DEFAULT_MAXIMUM_NESTING_DEPTH);
156+
this.keepNumberAsString = keepStrings;
157+
this.keepBooleanAsString = keepStrings;
146158
this.cDataTagName = cDataTagName;
147159
this.convertNilAttributeToNull = convertNilAttributeToNull;
148160
}
@@ -163,8 +175,10 @@ public XMLParserConfiguration (final boolean keepStrings, final String cDataTagN
163175
*/
164176
private XMLParserConfiguration (final boolean keepStrings, final String cDataTagName,
165177
final boolean convertNilAttributeToNull, final Map<String, XMLXsiTypeConverter<?>> xsiTypeMap, final Set<String> forceList,
166-
final int maxNestingDepth, final boolean closeEmptyTag) {
167-
super(keepStrings, maxNestingDepth);
178+
final int maxNestingDepth, final boolean closeEmptyTag, final boolean keepNumberAsString, final boolean keepBooleanAsString) {
179+
super(false, maxNestingDepth);
180+
this.keepNumberAsString = keepNumberAsString;
181+
this.keepBooleanAsString = keepBooleanAsString;
168182
this.cDataTagName = cDataTagName;
169183
this.convertNilAttributeToNull = convertNilAttributeToNull;
170184
this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap);
@@ -189,7 +203,9 @@ protected XMLParserConfiguration clone() {
189203
this.xsiTypeMap,
190204
this.forceList,
191205
this.maxNestingDepth,
192-
this.closeEmptyTag
206+
this.closeEmptyTag,
207+
this.keepNumberAsString,
208+
this.keepBooleanAsString
193209
);
194210
config.shouldTrimWhiteSpace = this.shouldTrimWhiteSpace;
195211
return config;
@@ -207,7 +223,40 @@ protected XMLParserConfiguration clone() {
207223
@SuppressWarnings("unchecked")
208224
@Override
209225
public XMLParserConfiguration withKeepStrings(final boolean newVal) {
210-
return super.withKeepStrings(newVal);
226+
XMLParserConfiguration newConfig = this.clone();
227+
newConfig.keepNumberAsString = newVal;
228+
newConfig.keepBooleanAsString = newVal;
229+
return newConfig;
230+
}
231+
232+
/**
233+
* When parsing the XML into JSON, specifies if numbers should be kept as strings (<code>1</code>), or if
234+
* they should try to be guessed into JSON values (numeric, boolean, string)
235+
*
236+
* @param newVal
237+
* new value to use for the <code>keepNumberAsString</code> configuration option.
238+
*
239+
* @return The existing configuration will not be modified. A new configuration is returned.
240+
*/
241+
public XMLParserConfiguration withKeepNumberAsString(final boolean newVal) {
242+
XMLParserConfiguration newConfig = this.clone();
243+
newConfig.keepNumberAsString = newVal;
244+
return newConfig;
245+
}
246+
247+
/**
248+
* When parsing the XML into JSON, specifies if booleans should be kept as strings (<code>true</code>), or if
249+
* they should try to be guessed into JSON values (numeric, boolean, string)
250+
*
251+
* @param newVal
252+
* new value to use for the <code>withKeepBooleanAsString</code> configuration option.
253+
*
254+
* @return The existing configuration will not be modified. A new configuration is returned.
255+
*/
256+
public XMLParserConfiguration withKeepBooleanAsString(final boolean newVal) {
257+
XMLParserConfiguration newConfig = this.clone();
258+
newConfig.keepBooleanAsString = newVal;
259+
return newConfig;
211260
}
212261

213262
/**
@@ -221,6 +270,26 @@ public String getcDataTagName() {
221270
return this.cDataTagName;
222271
}
223272

273+
/**
274+
* When parsing the XML into JSONML, specifies if numbers should be kept as strings (<code>true</code>), or if
275+
* they should try to be guessed into JSON values (numeric, boolean, string).
276+
*
277+
* @return The <code>keepStrings</code> configuration value.
278+
*/
279+
public boolean isKeepNumberAsString() {
280+
return this.keepNumberAsString;
281+
}
282+
283+
/**
284+
* When parsing the XML into JSONML, specifies if booleans should be kept as strings (<code>true</code>), or if
285+
* they should try to be guessed into JSON values (numeric, boolean, string).
286+
*
287+
* @return The <code>keepStrings</code> configuration value.
288+
*/
289+
public boolean isKeepBooleanAsString() {
290+
return this.keepBooleanAsString;
291+
}
292+
224293
/**
225294
* The name of the key in a JSON Object that indicates a CDATA section. Historically this has
226295
* been the value "content" but can be changed. Use <code>null</code> to indicate no CDATA

Diff for: src/test/java/org/json/junit/XMLConfigurationTest.java

+32-5
Original file line numberDiff line numberDiff line change
@@ -574,15 +574,18 @@ public void shouldKeepConfigurationIntactAndUpdateCloseEmptyTagChoice()
574574
XMLParserConfiguration keepStringsAndCloseEmptyTag = keepStrings.withCloseEmptyTag(true);
575575
XMLParserConfiguration keepDigits = keepStringsAndCloseEmptyTag.withKeepStrings(false);
576576
XMLParserConfiguration keepDigitsAndNoCloseEmptyTag = keepDigits.withCloseEmptyTag(false);
577-
assertTrue(keepStrings.isKeepStrings());
577+
assertTrue(keepStrings.isKeepNumberAsString());
578+
assertTrue(keepStrings.isKeepBooleanAsString());
578579
assertFalse(keepStrings.isCloseEmptyTag());
579-
assertTrue(keepStringsAndCloseEmptyTag.isKeepStrings());
580+
assertTrue(keepStringsAndCloseEmptyTag.isKeepNumberAsString());
581+
assertTrue(keepStringsAndCloseEmptyTag.isKeepBooleanAsString());
580582
assertTrue(keepStringsAndCloseEmptyTag.isCloseEmptyTag());
581-
assertFalse(keepDigits.isKeepStrings());
583+
assertFalse(keepDigits.isKeepNumberAsString());
584+
assertFalse(keepDigits.isKeepBooleanAsString());
582585
assertTrue(keepDigits.isCloseEmptyTag());
583-
assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepStrings());
586+
assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepNumberAsString());
587+
assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepBooleanAsString());
584588
assertFalse(keepDigitsAndNoCloseEmptyTag.isCloseEmptyTag());
585-
586589
}
587590

588591
/**
@@ -767,6 +770,30 @@ public void testToJSONArray_jsonOutput() {
767770
Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected);
768771
}
769772

773+
/**
774+
* JSON string lost leading zero and converted "True" to true.
775+
*/
776+
@Test
777+
public void testToJSONArray_jsonOutput_withKeepNumberAsString() {
778+
final String originalXml = "<root><id>01</id><id>1</id><id>00</id><id>0</id><item id=\"01\"/><title>True</title></root>";
779+
final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",\"1\",\"00\",\"0\"],\"title\":true}}");
780+
final JSONObject actualJsonOutput = XML.toJSONObject(originalXml,
781+
new XMLParserConfiguration().withKeepNumberAsString(true));
782+
Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected);
783+
}
784+
785+
/**
786+
* JSON string lost leading zero and converted "True" to true.
787+
*/
788+
@Test
789+
public void testToJSONArray_jsonOutput_withKeepBooleanAsString() {
790+
final String originalXml = "<root><id>01</id><id>1</id><id>00</id><id>0</id><item id=\"01\"/><title>True</title></root>";
791+
final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",1,\"00\",0],\"title\":\"True\"}}");
792+
final JSONObject actualJsonOutput = XML.toJSONObject(originalXml,
793+
new XMLParserConfiguration().withKeepBooleanAsString(true));
794+
Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected);
795+
}
796+
770797
/**
771798
* JSON string cannot be reverted to original xml.
772799
*/

0 commit comments

Comments
 (0)