Skip to content

Commit f972118

Browse files
jmesnilclaude
andcommitted
feat: add DataPart.fromJson() factory method for raw JSON strings
Allow developers to create a DataPart directly from a JSON string without manually deserializing it into Java objects first. This fixes #753 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c8ba7df commit f972118

2 files changed

Lines changed: 156 additions & 0 deletions

File tree

spec/src/main/java/io/a2a/spec/DataPart.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package io.a2a.spec;
22

33

4+
import com.google.gson.Gson;
5+
import com.google.gson.GsonBuilder;
6+
import com.google.gson.JsonSyntaxException;
7+
import com.google.gson.ToNumberPolicy;
48
import io.a2a.util.Assert;
59
import java.util.Map;
610
import org.jspecify.annotations.Nullable;
@@ -77,4 +81,59 @@ public DataPart (Object data, @Nullable Map<String, Object> metadata) {
7781
public DataPart(Object data) {
7882
this(data, null);
7983
}
84+
85+
/**
86+
* Creates a DataPart by parsing a JSON string into its corresponding Java type.
87+
* <p>
88+
* The JSON string is parsed using Gson with {@code ToNumberPolicy.LONG_OR_DOUBLE},
89+
* producing the following mappings:
90+
* <ul>
91+
* <li>JSON objects → {@code Map<String, Object>}</li>
92+
* <li>JSON arrays → {@code List<Object>}</li>
93+
* <li>JSON strings → {@code String}</li>
94+
* <li>JSON integers → {@code Long}</li>
95+
* <li>JSON decimals → {@code Double}</li>
96+
* <li>JSON booleans → {@code Boolean}</li>
97+
* </ul>
98+
* <p>
99+
* Example usage:
100+
* <pre>{@code
101+
* DataPart dataPart = DataPart.fromJson("""
102+
* {
103+
* "temperature": 22.5,
104+
* "humidity": 65
105+
* }""");
106+
* }</pre>
107+
*
108+
* @param json the JSON string to parse (must not be null or the JSON literal "null")
109+
* @return a new DataPart containing the parsed data
110+
* @throws IllegalArgumentException if json is null, parses to null, or is not valid
111+
*/
112+
public static DataPart fromJson(String json) {
113+
return fromJson(json, null);
114+
}
115+
116+
/**
117+
* Creates a DataPart by parsing a JSON string into its corresponding Java type,
118+
* with optional metadata.
119+
*
120+
* @param json the JSON string to parse (must not be null or the JSON literal "null")
121+
* @param metadata additional metadata for the part
122+
* @return a new DataPart containing the parsed data and metadata
123+
* @throws IllegalArgumentException if json is null, parses to null, or is not valid
124+
* @see #fromJson(String)
125+
*/
126+
public static DataPart fromJson(String json, @Nullable Map<String, Object> metadata) {
127+
Assert.checkNotNullParam("json", json);
128+
try {
129+
Object data = JSON_PARSER.fromJson(json, Object.class);
130+
return new DataPart(data, metadata);
131+
} catch (JsonSyntaxException e) {
132+
throw new IllegalArgumentException("Invalid JSON: " + json, e);
133+
}
134+
}
135+
136+
private static final Gson JSON_PARSER = new GsonBuilder()
137+
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
138+
.create();
80139
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package io.a2a.spec;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
5+
import static org.junit.jupiter.api.Assertions.assertNull;
6+
import static org.junit.jupiter.api.Assertions.assertThrows;
7+
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
import org.junit.jupiter.api.Test;
12+
13+
class DataPartTest {
14+
15+
@Test
16+
void testFromJson_object() {
17+
DataPart part = DataPart.fromJson("""
18+
{"temperature": 22.5, "humidity": 65}""");
19+
20+
Map<String, Object> data = assertInstanceOf(Map.class, part.data());
21+
assertEquals(22.5, data.get("temperature"));
22+
assertEquals(65L, data.get("humidity"));
23+
assertNull(part.metadata());
24+
}
25+
26+
@Test
27+
void testFromJson_array() {
28+
DataPart part = DataPart.fromJson("""
29+
["a", "b", "c"]""");
30+
31+
List<Object> data = assertInstanceOf(List.class, part.data());
32+
assertEquals(List.of("a", "b", "c"), data);
33+
}
34+
35+
@Test
36+
void testFromJson_string() {
37+
DataPart part = DataPart.fromJson("\"hello\"");
38+
39+
assertEquals("hello", part.data());
40+
}
41+
42+
@Test
43+
void testFromJson_integerNumber() {
44+
DataPart part = DataPart.fromJson("42");
45+
46+
assertEquals(42L, part.data());
47+
}
48+
49+
@Test
50+
void testFromJson_decimalNumber() {
51+
DataPart part = DataPart.fromJson("3.14");
52+
53+
assertEquals(3.14, part.data());
54+
}
55+
56+
@Test
57+
void testFromJson_boolean() {
58+
DataPart part = DataPart.fromJson("true");
59+
60+
assertEquals(true, part.data());
61+
}
62+
63+
@Test
64+
void testFromJson_withMetadata() {
65+
Map<String, Object> metadata = Map.of("source", "sensor");
66+
DataPart part = DataPart.fromJson("""
67+
{"temperature": 22.5}""", metadata);
68+
69+
assertInstanceOf(Map.class, part.data());
70+
assertEquals("sensor", part.metadata().get("source"));
71+
}
72+
73+
@Test
74+
void testFromJson_nestedObject() {
75+
DataPart part = DataPart.fromJson("""
76+
{"outer": {"inner": [1, 2, 3]}}""");
77+
78+
Map<String, Object> data = assertInstanceOf(Map.class, part.data());
79+
Map<String, Object> outer = assertInstanceOf(Map.class, data.get("outer"));
80+
assertEquals(List.of(1L, 2L, 3L), outer.get("inner"));
81+
}
82+
83+
@Test
84+
void testFromJson_nullJsonThrows() {
85+
assertThrows(IllegalArgumentException.class, () -> DataPart.fromJson(null));
86+
}
87+
88+
@Test
89+
void testFromJson_nullLiteralThrows() {
90+
assertThrows(IllegalArgumentException.class, () -> DataPart.fromJson("null"));
91+
}
92+
93+
@Test
94+
void testFromJson_invalidJsonThrows() {
95+
assertThrows(IllegalArgumentException.class, () -> DataPart.fromJson("{invalid}"));
96+
}
97+
}

0 commit comments

Comments
 (0)