1313import static io .a2a .spec .A2AErrorCodes .TASK_NOT_FOUND_ERROR_CODE ;
1414import static io .a2a .spec .A2AErrorCodes .UNSUPPORTED_OPERATION_ERROR_CODE ;
1515import static io .a2a .spec .DataPart .DATA ;
16- import static io .a2a .spec .FilePart .FILE ;
1716import static io .a2a .spec .TextPart .TEXT ;
1817import static java .lang .String .format ;
1918import static java .util .Collections .emptyMap ;
@@ -521,33 +520,58 @@ public static Map<String, Object> readMetadata(@Nullable String json) throws Jso
521520 */
522521 static class PartTypeAdapter extends TypeAdapter <Part <?>> {
523522
524- private static final Set <String > VALID_KEYS = Set .of (TEXT , FILE , DATA );
523+ private static final String RAW = "raw" ;
524+ private static final String URL = "url" ;
525+ private static final String FILENAME = "filename" ;
526+ private static final String MEDIA_TYPE = "mediaType" ;
527+ // The oneOf content-type discriminator keys in the flat JSON format.
528+ // Exactly one must be present (and non-null) in each Part object.
529+ private static final Set <String > VALID_KEYS = Set .of (TEXT , RAW , URL , DATA );
525530 private static final Type MAP_TYPE = new TypeToken <Map <String , Object >>(){}.getType ();
526531
527532 // Create separate Gson instance without the Part adapter to avoid recursion
528533 private final Gson delegateGson = createBaseGsonBuilder ().create ();
529534
535+ private void writeMetadata (JsonWriter out , @ Nullable Map <String , Object > metadata ) throws java .io .IOException {
536+ if (metadata != null && !metadata .isEmpty ()) {
537+ out .name ("metadata" );
538+ delegateGson .toJson (metadata , MAP_TYPE , out );
539+ }
540+ }
541+
542+ /** Writes a string field only when the value is non-null and non-empty. */
543+ private void writeNonEmpty (JsonWriter out , String name , String value ) throws java .io .IOException {
544+ if (!value .isEmpty ()) {
545+ out .name (name ).value (value );
546+ }
547+ }
548+
530549 @ Override
531550 public void write (JsonWriter out , Part <?> value ) throws java .io .IOException {
532551 if (value == null ) {
533552 out .nullValue ();
534553 return ;
535554 }
536- // Write wrapper object with member name as discriminator
537555 out .beginObject ();
538556
539557 if (value instanceof TextPart textPart ) {
540- // TextPart: { "text": "value" } - direct string value
541- out .name (TEXT );
542- out .value (textPart .text ());
543- JsonUtil .writeMetadata (out , textPart .metadata ());
558+ out .name (TEXT ).value (textPart .text ());
559+ writeMetadata (out , textPart .metadata ());
544560 } else if (value instanceof FilePart filePart ) {
545- // FilePart: { "file": {...} }
546- out .name (FILE );
547- delegateGson .toJson (filePart .file (), FileContent .class , out );
548- JsonUtil .writeMetadata (out , filePart .metadata ());
561+ if (filePart .file () instanceof FileWithBytes withBytes ) {
562+ out .name (RAW ).value (withBytes .bytes ());
563+ writeNonEmpty (out , FILENAME , withBytes .name ());
564+ writeNonEmpty (out , MEDIA_TYPE , withBytes .mimeType ());
565+ } else if (filePart .file () instanceof FileWithUri withUri ) {
566+ out .name (URL ).value (withUri .uri ());
567+ writeNonEmpty (out , FILENAME , withUri .name ());
568+ writeNonEmpty (out , MEDIA_TYPE , withUri .mimeType ());
569+ } else {
570+ throw new JsonSyntaxException ("Unknown FileContent subclass: " + filePart .file ().getClass ().getName ());
571+ }
572+ writeMetadata (out , filePart .metadata ());
573+
549574 } else if (value instanceof DataPart dataPart ) {
550- // DataPart: { "data": <any JSON value> }
551575 out .name (DATA );
552576 delegateGson .toJson (dataPart .data (), Object .class , out );
553577 JsonUtil .writeMetadata (out , dataPart .metadata ());
@@ -566,7 +590,6 @@ Part<?> read(JsonReader in) throws java.io.IOException {
566590 return null ;
567591 }
568592
569- // Read the JSON as a tree to inspect the member name discriminator
570593 com .google .gson .JsonElement jsonElement = com .google .gson .JsonParser .parseReader (in );
571594 if (!jsonElement .isJsonObject ()) {
572595 throw new JsonSyntaxException ("Part must be a JSON object" );
@@ -576,34 +599,47 @@ Part<?> read(JsonReader in) throws java.io.IOException {
576599
577600 // Extract metadata if present
578601 Map <String , Object > metadata = JsonUtil .readMetadata (jsonObject );
579-
580- // Check for member name discriminators (v1.0 protocol)
581602 Set <String > keys = jsonObject .keySet ();
582- if (keys .size () < 1 || keys .size () > 2 ) {
583- throw new JsonSyntaxException (format ("Part object must have one content key from %s and optionally 'metadata' (found: %s)" , VALID_KEYS , keys ));
584- }
585603
586- // Find the discriminator (should be one of TEXT, FILE, DATA)
604+ // Find the oneOf discriminator, skipping null/empty values to tolerate formats
605+ // where multiple content keys may be present with only one populated
606+ // (e.g., proto serialization with alwaysPrintFieldsWithNoPresence).
607+ // Unknown extra fields are ignored.
587608 String discriminator = keys .stream ()
588609 .filter (VALID_KEYS ::contains )
610+ .filter (key -> {
611+ com .google .gson .JsonElement el = jsonObject .get (key );
612+ return el != null && !el .isJsonNull ();
613+ })
589614 .findFirst ()
590615 .orElseThrow (() -> new JsonSyntaxException (format ("Part must have one of: %s (found: %s)" , VALID_KEYS , keys )));
591616
592617 return switch (discriminator ) {
593618 case TEXT -> new TextPart (jsonObject .get (TEXT ).getAsString (), metadata );
594- case FILE -> new FilePart (delegateGson .fromJson (jsonObject .get (FILE ), FileContent .class ), metadata );
619+ case RAW -> new FilePart (new FileWithBytes (
620+ stringOrEmpty (jsonObject , MEDIA_TYPE ),
621+ stringOrEmpty (jsonObject , FILENAME ),
622+ jsonObject .get (RAW ).getAsString ()), metadata );
623+ case URL -> new FilePart (new FileWithUri (
624+ stringOrEmpty (jsonObject , MEDIA_TYPE ),
625+ stringOrEmpty (jsonObject , FILENAME ),
626+ jsonObject .get (URL ).getAsString ()), metadata );
595627 case DATA -> {
596- // DataPart supports any JSON value: object, array, primitive, or null
597- Object data = delegateGson .fromJson (
598- jsonObject .get (DATA ),
599- Object .class
600- );
628+ Object data = delegateGson .fromJson (jsonObject .get (DATA ), Object .class );
601629 yield new DataPart (data , metadata );
602630 }
603- default ->
604- throw new JsonSyntaxException (format ("Part must have one of: %s (found: %s)" , VALID_KEYS , discriminator ));
631+ default -> throw new JsonSyntaxException (format ("Part must have one of: %s (found: %s)" , VALID_KEYS , discriminator ));
605632 };
606633 }
634+
635+ /** Returns the string value of the field, or an empty string if absent or null. */
636+ private String stringOrEmpty (com .google .gson .JsonObject obj , String key ) {
637+ com .google .gson .JsonElement el = obj .get (key );
638+ if (el == null || el .isJsonNull ()) {
639+ return "" ;
640+ }
641+ return el .getAsString ();
642+ }
607643 }
608644
609645 /**
0 commit comments