Skip to content

Commit 3a659c5

Browse files
committed
feat(json): Add String mapping support
1 parent b1ef6fc commit 3a659c5

File tree

3 files changed

+109
-53
lines changed

3 files changed

+109
-53
lines changed

components/json/json-io/src/main/java/datadog/json/buffer/JsonMapper.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,29 @@
22

33
import java.util.Map;
44

5-
/** Utility class for simple Java structure mapping into JSON strings. */
5+
/**
6+
* Utility class for simple Java structure mapping into JSON strings.
7+
*/
68
public final class JsonMapper {
79

8-
private JsonMapper() {}
10+
private JsonMapper() {
11+
}
12+
13+
/**
14+
* Converts a {@link String} to a JSON string.
15+
*
16+
* @param string The string to convert.
17+
* @return The converted JSON string.
18+
*/
19+
public static String toJson(String string) {
20+
if (string == null || string.isEmpty()) {
21+
return "";
22+
}
23+
try (JsonWriter writer = new JsonWriter()) {
24+
writer.value(string);
25+
return writer.toString();
26+
}
27+
}
928

1029
/**
1130
* Converts a {@link Map} to a JSON object.

components/json/json-io/src/main/java/datadog/json/buffer/JsonWriter.java

Lines changed: 46 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.io.Flushable;
77
import java.io.IOException;
88
import java.io.OutputStreamWriter;
9+
import java.util.Locale;
910

1011
/**
1112
* A lightweight JSON writer without dependencies. It performs minimal JSON structure check unless
@@ -271,45 +272,51 @@ private void writeStringLiteral(String str) {
271272
this.writer.write('"');
272273

273274
for (int i = 0; i < str.length(); ++i) {
274-
char ch = str.charAt(i);
275-
276-
// Based on https://keploy.io/blog/community/json-escape-and-unescape
277-
switch (ch) {
278-
case '"':
279-
this.writer.write("\\\"");
280-
break;
281-
282-
case '\\':
283-
this.writer.write("\\\\");
284-
break;
285-
286-
case '/':
287-
this.writer.write("\\/");
288-
break;
289-
290-
case '\b':
291-
this.writer.write("\\b");
292-
break;
293-
294-
case '\f':
295-
this.writer.write("\\f");
296-
break;
297-
298-
case '\n':
299-
this.writer.write("\\n");
300-
break;
301-
302-
case '\r':
303-
this.writer.write("\\r");
304-
break;
305-
306-
case '\t':
307-
this.writer.write("\\t");
308-
break;
309-
310-
default:
311-
this.writer.write(ch);
312-
break;
275+
char c = str.charAt(i);
276+
// Escape any char outside ASCII to their Unicode equivalent
277+
if (c > 127) {
278+
this.writer.write('\\');
279+
this.writer.write('u');
280+
String hexCharacter = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
281+
if (c < 4096) {
282+
this.writer.write('0');
283+
if (c < 256) {
284+
this.writer.write('0');
285+
}
286+
}
287+
this.writer.append(hexCharacter);
288+
} else {
289+
switch (c) {
290+
case '"': // Quotation mark
291+
case '\\': // Reverse solidus
292+
case '/': // Solidus
293+
this.writer.write('\\');
294+
this.writer.write(c);
295+
break;
296+
case '\b': // Backspace
297+
this.writer.write('\\');
298+
this.writer.write('b');
299+
break;
300+
case '\f': // Form feed
301+
this.writer.write('\\');
302+
this.writer.write('f');
303+
break;
304+
case '\n': // Line feed
305+
this.writer.write('\\');
306+
this.writer.write('n');
307+
break;
308+
case '\r': // Carriage return
309+
this.writer.write('\\');
310+
this.writer.write('r');
311+
break;
312+
case '\t': // Horizontal tab
313+
this.writer.write('\\');
314+
this.writer.write('t');
315+
break;
316+
default:
317+
this.writer.write(c);
318+
break;
319+
}
313320
}
314321
}
315322

@@ -325,14 +332,4 @@ private void writeStringRaw(String str) {
325332
// ignore
326333
}
327334
}
328-
329-
private JsonWriter writeBytesRaw(byte[] bytes) {
330-
try {
331-
writer.flush();
332-
outputStream.write(bytes);
333-
} catch (IOException e) {
334-
// ignore
335-
}
336-
return this;
337-
}
338335
}

components/json/json-io/src/test/groovy/datadog/json/buffer/JsonMapperTest.groovy

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import spock.lang.Specification
44

55
class JsonMapperTest extends Specification {
66

7-
def "test map toJson: #input"() {
7+
def "test mapping to JSON object: #input"() {
88
when:
99
String json = JsonMapper.toJson((Map) input)
1010

@@ -20,7 +20,7 @@ class JsonMapperTest extends Specification {
2020
['key1': 'va"lu"e1', 'ke"y2': 'value2'] | "{\"key1\":\"va\\\"lu\\\"e1\",\"ke\\\"y2\":\"value2\"}"
2121
}
2222

23-
def "test iterable toJson: #input"() {
23+
def "test mapping iterable to JSON array: #input"() {
2424
when:
2525
String json = JsonMapper.toJson((Iterable) input)
2626

@@ -35,4 +35,44 @@ class JsonMapperTest extends Specification {
3535
['value1', 'value2'] | "[\"value1\",\"value2\"]"
3636
['va"lu"e1', 'value2'] | "[\"va\\\"lu\\\"e1\",\"value2\"]"
3737
}
38+
39+
def "test mapping array to JSON array: #input"() {
40+
when:
41+
String json = JsonMapper.toJson((String[]) input)
42+
43+
then:
44+
json == expected
45+
46+
where:
47+
input | expected
48+
[] | "[]"
49+
['value1'] | "[\"value1\"]"
50+
['value1', 'value2'] | "[\"value1\",\"value2\"]"
51+
['va"lu"e1', 'value2'] | "[\"va\\\"lu\\\"e1\",\"value2\"]"
52+
}
53+
54+
def "test mapping to JSON string: input"() {
55+
when:
56+
String escaped = JsonMapper.toJson((String) string)
57+
58+
then:
59+
escaped == expected
60+
61+
where:
62+
string | expected
63+
null | ""
64+
"" | ""
65+
((char) 4096).toString() | '"\\u1000"'
66+
((char) 256).toString() | '"\\u0100"'
67+
((char) 128).toString() | '"\\u0080"'
68+
"\b" | '"\\b"'
69+
"\t" | '"\\t"'
70+
"\n" | '"\\n"'
71+
"\f" | '"\\f"'
72+
"\r" | '"\\r"'
73+
'"' | '"\\\""'
74+
'/' | '"\\/"'
75+
'\\' | '"\\\\"'
76+
"a" | '"a"'
77+
}
3878
}

0 commit comments

Comments
 (0)