Skip to content

Commit 77e3e3b

Browse files
committed
chore: make JsonUtil methods more consistent
Signed-off-by: Jeff Mesnil <[email protected]>
1 parent 42545b1 commit 77e3e3b

File tree

1 file changed

+85
-60
lines changed
  • jsonrpc-common/src/main/java/io/a2a/jsonrpc/common/json

1 file changed

+85
-60
lines changed

jsonrpc-common/src/main/java/io/a2a/jsonrpc/common/json/JsonUtil.java

Lines changed: 85 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
import static io.a2a.spec.A2AErrorCodes.TASK_NOT_CANCELABLE_ERROR_CODE;
1313
import static io.a2a.spec.A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE;
1414
import static io.a2a.spec.A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE;
15+
import static io.a2a.spec.DataPart.DATA;
16+
import static io.a2a.spec.FilePart.FILE;
17+
import static io.a2a.spec.TextPart.TEXT;
18+
import static java.lang.String.format;
1519

1620
import java.io.StringReader;
17-
import java.lang.InternalError;
1821
import java.lang.reflect.Type;
1922
import java.time.OffsetDateTime;
2023
import java.time.format.DateTimeFormatter;
@@ -32,7 +35,36 @@
3235
import com.google.gson.stream.JsonToken;
3336
import com.google.gson.stream.JsonWriter;
3437

35-
import io.a2a.spec.*;
38+
import io.a2a.spec.A2AError;
39+
import io.a2a.spec.APIKeySecurityScheme;
40+
import io.a2a.spec.ContentTypeNotSupportedError;
41+
import io.a2a.spec.DataPart;
42+
import io.a2a.spec.FileContent;
43+
import io.a2a.spec.FilePart;
44+
import io.a2a.spec.FileWithBytes;
45+
import io.a2a.spec.FileWithUri;
46+
import io.a2a.spec.HTTPAuthSecurityScheme;
47+
import io.a2a.spec.InvalidAgentResponseError;
48+
import io.a2a.spec.InvalidParamsError;
49+
import io.a2a.spec.InvalidRequestError;
50+
import io.a2a.spec.JSONParseError;
51+
import io.a2a.spec.Message;
52+
import io.a2a.spec.MethodNotFoundError;
53+
import io.a2a.spec.MutualTLSSecurityScheme;
54+
import io.a2a.spec.OAuth2SecurityScheme;
55+
import io.a2a.spec.OpenIdConnectSecurityScheme;
56+
import io.a2a.spec.Part;
57+
import io.a2a.spec.PushNotificationNotSupportedError;
58+
import io.a2a.spec.SecurityScheme;
59+
import io.a2a.spec.StreamingEventKind;
60+
import io.a2a.spec.Task;
61+
import io.a2a.spec.TaskArtifactUpdateEvent;
62+
import io.a2a.spec.TaskNotCancelableError;
63+
import io.a2a.spec.TaskNotFoundError;
64+
import io.a2a.spec.TaskState;
65+
import io.a2a.spec.TaskStatusUpdateEvent;
66+
import io.a2a.spec.TextPart;
67+
import io.a2a.spec.UnsupportedOperationError;
3668

3769
import org.jspecify.annotations.Nullable;
3870

@@ -512,6 +544,8 @@ public void write(JsonWriter out, Message.Role value) throws java.io.IOException
512544
*/
513545
static class PartTypeAdapter extends TypeAdapter<Part<?>> {
514546

547+
private static final Set<String> VALID_KEYS = Set.of(TEXT, FILE, DATA);
548+
515549
// Create separate Gson instance without the Part adapter to avoid recursion
516550
private final Gson delegateGson = createBaseGsonBuilder().create();
517551

@@ -521,21 +555,20 @@ public void write(JsonWriter out, Part<?> value) throws java.io.IOException {
521555
out.nullValue();
522556
return;
523557
}
524-
525558
// Write wrapper object with member name as discriminator
526559
out.beginObject();
527560

528561
if (value instanceof TextPart textPart) {
529562
// TextPart: { "text": "value" } - direct string value
530-
out.name("text");
563+
out.name(TEXT);
531564
out.value(textPart.text());
532565
} else if (value instanceof FilePart filePart) {
533566
// FilePart: { "file": {...} }
534-
out.name("file");
567+
out.name(FILE);
535568
delegateGson.toJson(filePart.file(), FileContent.class, out);
536569
} else if (value instanceof DataPart dataPart) {
537570
// DataPart: { "data": {...} }
538-
out.name("data");
571+
out.name(DATA);
539572
delegateGson.toJson(dataPart.data(), Map.class, out);
540573
} else {
541574
throw new JsonSyntaxException("Unknown Part subclass: " + value.getClass().getName());
@@ -561,23 +594,27 @@ Part<?> read(JsonReader in) throws java.io.IOException {
561594
com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject();
562595

563596
// Check for member name discriminators (v1.0 protocol)
564-
if (jsonObject.has("text")) {
565-
// TextPart: { "text": "value" } - direct string value
566-
return new TextPart(jsonObject.get("text").getAsString());
567-
} else if (jsonObject.has("file")) {
568-
// FilePart: { "file": {...} }
569-
return new FilePart(delegateGson.fromJson(jsonObject.get("file"), FileContent.class));
570-
} else if (jsonObject.has("data")) {
571-
// DataPart: { "data": {...} }
572-
@SuppressWarnings("unchecked")
573-
Map<String, Object> dataMap = delegateGson.fromJson(
574-
jsonObject.get("data"),
575-
new TypeToken<Map<String, Object>>(){}.getType()
576-
);
577-
return new DataPart(dataMap);
578-
} else {
579-
throw new JsonSyntaxException("Part must have one of: text, file, data (found: " + jsonObject.keySet() + ")");
580-
}
597+
Set<String> keys = jsonObject.keySet();
598+
if (keys.size() != 1) {
599+
throw new JsonSyntaxException(format("Part object must have exactly one key, which must be one of: %s (found: %s)", VALID_KEYS, keys));
600+
}
601+
602+
String discriminator = keys.iterator().next();
603+
604+
return switch (discriminator) {
605+
case TEXT -> new TextPart(jsonObject.get(TEXT).getAsString());
606+
case FILE -> new FilePart(delegateGson.fromJson(jsonObject.get(FILE), FileContent.class));
607+
case DATA -> {
608+
Map<String, Object> dataMap = delegateGson.fromJson(
609+
jsonObject.get(DATA),
610+
new TypeToken<Map<String, Object>>(){}.getType()
611+
);
612+
yield new DataPart(dataMap);
613+
}
614+
default ->
615+
// This case should be unreachable due to the keySet check above.
616+
throw new JsonSyntaxException(format("Part must have one of: %s (found: %s)", VALID_KEYS, discriminator));
617+
};
581618
}
582619
}
583620

@@ -609,20 +646,10 @@ public void write(JsonWriter out, StreamingEventKind value) throws java.io.IOExc
609646
out.nullValue();
610647
return;
611648
}
612-
613649
// Write wrapper object with member name as discriminator
614650
out.beginObject();
615-
616-
Type type = switch (value.kind()) {
617-
case Task.STREAMING_EVENT_ID -> Task.class;
618-
case Message.STREAMING_EVENT_ID -> Message.class;
619-
case TaskStatusUpdateEvent.STREAMING_EVENT_ID -> TaskStatusUpdateEvent.class;
620-
case TaskArtifactUpdateEvent.STREAMING_EVENT_ID -> TaskArtifactUpdateEvent.class;
621-
default -> throw new JsonSyntaxException("Unknown StreamingEventKind implementation: " + value.getClass().getName());
622-
};
623-
624651
out.name(value.kind());
625-
delegateGson.toJson(value, type, out);
652+
delegateGson.toJson(value, value.getClass(), out);
626653
out.endObject();
627654
}
628655

@@ -705,13 +732,7 @@ public void write(JsonWriter out, FileContent value) throws java.io.IOException
705732
return;
706733
}
707734
// Delegate to Gson's default serialization for the concrete type
708-
if (value instanceof FileWithBytes fileWithBytes) {
709-
delegateGson.toJson(fileWithBytes, FileWithBytes.class, out);
710-
} else if (value instanceof FileWithUri fileWithUri) {
711-
delegateGson.toJson(fileWithUri, FileWithUri.class, out);
712-
} else {
713-
throw new JsonSyntaxException("Unknown FileContent implementation: " + value.getClass().getName());
714-
}
735+
delegateGson.toJson(value, value.getClass(), out);
715736
}
716737

717738
@Override
@@ -758,11 +779,7 @@ static class APIKeyLocationTypeAdapter extends TypeAdapter<APIKeySecurityScheme.
758779

759780
@Override
760781
public void write(JsonWriter out, APIKeySecurityScheme.Location value) throws java.io.IOException {
761-
if (value == null) {
762-
out.nullValue();
763-
} else {
764-
out.value(value.asString());
765-
}
782+
out.value(value.asString());
766783
}
767784

768785
@Override
@@ -827,6 +844,12 @@ public void write(JsonWriter out, APIKeySecurityScheme.Location value) throws ja
827844
*/
828845
static class SecuritySchemeTypeAdapter extends TypeAdapter<SecurityScheme> {
829846

847+
private static final Set<String> VALID_KEYS = Set.of(APIKeySecurityScheme.TYPE,
848+
HTTPAuthSecurityScheme.TYPE,
849+
OAuth2SecurityScheme.TYPE,
850+
OpenIdConnectSecurityScheme.TYPE,
851+
MutualTLSSecurityScheme.TYPE);
852+
830853
// Create separate Gson instance without the SecurityScheme adapter to avoid recursion
831854
// Register custom adapter for APIKeySecurityScheme.Location enum
832855
private final Gson delegateGson = new GsonBuilder()
@@ -837,7 +860,7 @@ static class SecuritySchemeTypeAdapter extends TypeAdapter<SecurityScheme> {
837860
public void write(JsonWriter out, SecurityScheme value) throws java.io.IOException {
838861
// Write wrapper object with member name as discriminator
839862
out.beginObject();
840-
out.name(value.getType());
863+
out.name(value.type());
841864
delegateGson.toJson(value, value.getClass(), out);
842865
out.endObject();
843866
}
@@ -859,20 +882,22 @@ SecurityScheme read(JsonReader in) throws java.io.IOException {
859882
com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject();
860883

861884
// Check for member name discriminators
862-
if (jsonObject.has(APIKeySecurityScheme.TYPE)) {
863-
return delegateGson.fromJson(jsonObject.get(APIKeySecurityScheme.TYPE), APIKeySecurityScheme.class);
864-
} else if (jsonObject.has(HTTPAuthSecurityScheme.TYPE)) {
865-
return delegateGson.fromJson(jsonObject.get(HTTPAuthSecurityScheme.TYPE), HTTPAuthSecurityScheme.class);
866-
} else if (jsonObject.has(OAuth2SecurityScheme.TYPE)) {
867-
return delegateGson.fromJson(jsonObject.get(OAuth2SecurityScheme.TYPE), OAuth2SecurityScheme.class);
868-
} else if (jsonObject.has(OpenIdConnectSecurityScheme.TYPE)) {
869-
return delegateGson.fromJson(jsonObject.get(OpenIdConnectSecurityScheme.TYPE), OpenIdConnectSecurityScheme.class);
870-
} else if (jsonObject.has(MutualTLSSecurityScheme.TYPE)) {
871-
return delegateGson.fromJson(jsonObject.get(MutualTLSSecurityScheme.TYPE), MutualTLSSecurityScheme.class);
872-
} else {
873-
throw new JsonSyntaxException(String.format("SecurityScheme must have one of: %s (found: %s)", SecurityScheme.VALID_TYPES, jsonObject.keySet()));
885+
Set<String> keys = jsonObject.keySet();
886+
if (keys.size() != 1) {
887+
throw new JsonSyntaxException(format("A SecurityScheme object must have exactly one key, which must be one of: %s (found: %s)", VALID_KEYS, keys));
874888
}
889+
890+
String discriminator = keys.iterator().next();
891+
com.google.gson.JsonElement nestedObject = jsonObject.get(discriminator);
892+
893+
return switch (discriminator) {
894+
case APIKeySecurityScheme.TYPE -> delegateGson.fromJson(nestedObject, APIKeySecurityScheme.class);
895+
case HTTPAuthSecurityScheme.TYPE -> delegateGson.fromJson(nestedObject, HTTPAuthSecurityScheme.class);
896+
case OAuth2SecurityScheme.TYPE -> delegateGson.fromJson(nestedObject, OAuth2SecurityScheme.class);
897+
case OpenIdConnectSecurityScheme.TYPE -> delegateGson.fromJson(nestedObject, OpenIdConnectSecurityScheme.class);
898+
case MutualTLSSecurityScheme.TYPE -> delegateGson.fromJson(nestedObject, MutualTLSSecurityScheme.class);
899+
default -> throw new JsonSyntaxException(format("Unknown SecurityScheme type. Must be one of: %s (found: %s)", VALID_KEYS, discriminator));
900+
};
875901
}
876902
}
877-
878903
}

0 commit comments

Comments
 (0)