1212import static io .a2a .spec .A2AErrorCodes .TASK_NOT_CANCELABLE_ERROR_CODE ;
1313import static io .a2a .spec .A2AErrorCodes .TASK_NOT_FOUND_ERROR_CODE ;
1414import 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
1620import java .io .StringReader ;
17- import java .lang .InternalError ;
1821import java .lang .reflect .Type ;
1922import java .time .OffsetDateTime ;
2023import java .time .format .DateTimeFormatter ;
3235import com .google .gson .stream .JsonToken ;
3336import 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
3769import 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