1919 *
2020 * Validates that the content is a valid Agent Card JSON document following the A2A protocol specification.
2121 *
22+ * Validation levels:
23+ * - NONE: No validation
24+ * - SYNTAX_ONLY: Validates that the content is valid JSON and is an object
25+ * - FULL: Full schema validation including required fields, type checking, and structure validation
26+ *
2227 * @see <a href="https://a2a-protocol.org/">A2A Protocol</a>
2328 */
2429public class AgentCardContentValidator implements ContentValidator {
@@ -42,60 +47,231 @@ public void validate(ValidityLevel level, TypedContent content,
4247 Collections .singleton (new RuleViolation ("Agent Card must be a JSON object" , "" )));
4348 }
4449
45- // SYNTAX_ONLY level: just check it's valid JSON (already done by parsing )
50+ // SYNTAX_ONLY level: just check it's valid JSON object (already done)
4651 if (level == ValidityLevel .SYNTAX_ONLY ) {
4752 return ;
4853 }
4954
50- // FULL level: validate required fields
51- if (!tree .has ("name" ) || tree .get ("name" ).asText ().trim ().isEmpty ()) {
52- violations .add (new RuleViolation ("Agent Card must have a non-empty 'name' field" , "/name" ));
55+ // FULL level: comprehensive validation
56+ validateRequiredFields (tree , violations );
57+ validateStringFields (tree , violations );
58+ validateProviderField (tree , violations );
59+ validateCapabilitiesField (tree , violations );
60+ validateSkillsField (tree , violations );
61+ validateArrayFields (tree , violations );
62+ validateAuthenticationField (tree , violations );
63+
64+ if (!violations .isEmpty ()) {
65+ throw new RuleViolationException ("Invalid Agent Card" , RuleType .VALIDITY , level .name (),
66+ violations );
5367 }
5468
55- // Validate optional but typed fields if present
56- if (tree .has ("version" ) && !tree .get ("version" ).isTextual ()) {
57- violations .add (new RuleViolation ("'version' field must be a string" , "/version" ));
69+ } catch (RuleViolationException e ) {
70+ throw e ;
71+ } catch (Exception e ) {
72+ throw new RuleViolationException ("Invalid Agent Card JSON: " + e .getMessage (),
73+ RuleType .VALIDITY , level .name (), e );
74+ }
75+ }
76+
77+ private void validateRequiredFields (JsonNode tree , Set <RuleViolation > violations ) {
78+ // 'name' is required and must be non-empty
79+ if (!tree .has ("name" )) {
80+ violations .add (new RuleViolation ("Agent Card must have a 'name' field" , "/name" ));
81+ } else if (!tree .get ("name" ).isTextual ()) {
82+ violations .add (new RuleViolation ("'name' field must be a string" , "/name" ));
83+ } else if (tree .get ("name" ).asText ().trim ().isEmpty ()) {
84+ violations .add (new RuleViolation ("'name' field must not be empty" , "/name" ));
85+ }
86+ }
87+
88+ private void validateStringFields (JsonNode tree , Set <RuleViolation > violations ) {
89+ // Validate optional string fields
90+ validateOptionalString (tree , "description" , violations );
91+ validateOptionalString (tree , "version" , violations );
92+ validateOptionalString (tree , "url" , violations );
93+ }
94+
95+ private void validateOptionalString (JsonNode tree , String fieldName , Set <RuleViolation > violations ) {
96+ if (tree .has (fieldName ) && !tree .get (fieldName ).isTextual ()) {
97+ violations .add (new RuleViolation ("'" + fieldName + "' field must be a string" , "/" + fieldName ));
98+ }
99+ }
100+
101+ private void validateProviderField (JsonNode tree , Set <RuleViolation > violations ) {
102+ if (!tree .has ("provider" )) {
103+ return ;
104+ }
105+
106+ JsonNode provider = tree .get ("provider" );
107+ if (!provider .isObject ()) {
108+ violations .add (new RuleViolation ("'provider' field must be an object" , "/provider" ));
109+ return ;
110+ }
111+
112+ if (provider .has ("organization" ) && !provider .get ("organization" ).isTextual ()) {
113+ violations .add (new RuleViolation ("'provider.organization' must be a string" , "/provider/organization" ));
114+ }
115+
116+ if (provider .has ("url" ) && !provider .get ("url" ).isTextual ()) {
117+ violations .add (new RuleViolation ("'provider.url' must be a string" , "/provider/url" ));
118+ }
119+ }
120+
121+ private void validateCapabilitiesField (JsonNode tree , Set <RuleViolation > violations ) {
122+ if (!tree .has ("capabilities" )) {
123+ return ;
124+ }
125+
126+ JsonNode capabilities = tree .get ("capabilities" );
127+ if (!capabilities .isObject ()) {
128+ violations .add (new RuleViolation ("'capabilities' field must be an object" , "/capabilities" ));
129+ return ;
130+ }
131+
132+ // Validate known capability fields are booleans
133+ validateCapabilityBoolean (capabilities , "streaming" , violations );
134+ validateCapabilityBoolean (capabilities , "pushNotifications" , violations );
135+ }
136+
137+ private void validateCapabilityBoolean (JsonNode capabilities , String fieldName , Set <RuleViolation > violations ) {
138+ if (capabilities .has (fieldName ) && !capabilities .get (fieldName ).isBoolean ()) {
139+ violations .add (new RuleViolation ("'capabilities." + fieldName + "' must be a boolean" ,
140+ "/capabilities/" + fieldName ));
141+ }
142+ }
143+
144+ private void validateSkillsField (JsonNode tree , Set <RuleViolation > violations ) {
145+ if (!tree .has ("skills" )) {
146+ return ;
147+ }
148+
149+ JsonNode skills = tree .get ("skills" );
150+ if (!skills .isArray ()) {
151+ violations .add (new RuleViolation ("'skills' field must be an array" , "/skills" ));
152+ return ;
153+ }
154+
155+ int index = 0 ;
156+ for (JsonNode skill : skills ) {
157+ String basePath = "/skills/" + index ;
158+
159+ if (!skill .isObject ()) {
160+ violations .add (new RuleViolation ("Skill at index " + index + " must be an object" , basePath ));
161+ index ++;
162+ continue ;
58163 }
59164
60- if (tree .has ("url" ) && !tree .get ("url" ).isTextual ()) {
61- violations .add (new RuleViolation ("'url' field must be a string" , "/url" ));
165+ // 'id' is required for skills
166+ if (!skill .has ("id" )) {
167+ violations .add (new RuleViolation ("Skill at index " + index + " must have an 'id' field" ,
168+ basePath + "/id" ));
169+ } else if (!skill .get ("id" ).isTextual ()) {
170+ violations .add (new RuleViolation ("Skill 'id' must be a string" , basePath + "/id" ));
171+ } else if (skill .get ("id" ).asText ().trim ().isEmpty ()) {
172+ violations .add (new RuleViolation ("Skill 'id' must not be empty" , basePath + "/id" ));
62173 }
63174
64- if (tree .has ("capabilities" ) && !tree .get ("capabilities" ).isObject ()) {
65- violations .add (new RuleViolation ("'capabilities' field must be an object" , "/capabilities" ));
175+ // 'name' is required for skills
176+ if (!skill .has ("name" )) {
177+ violations .add (new RuleViolation ("Skill at index " + index + " must have a 'name' field" ,
178+ basePath + "/name" ));
179+ } else if (!skill .get ("name" ).isTextual ()) {
180+ violations .add (new RuleViolation ("Skill 'name' must be a string" , basePath + "/name" ));
66181 }
67182
68- if (tree .has ("skills" ) && !tree .get ("skills" ).isArray ()) {
69- violations .add (new RuleViolation ("'skills' field must be an array" , "/skills" ));
183+ // Optional fields validation
184+ if (skill .has ("description" ) && !skill .get ("description" ).isTextual ()) {
185+ violations .add (new RuleViolation ("Skill 'description' must be a string" , basePath + "/description" ));
70186 }
71187
72- if (tree .has ("defaultInputModes" ) && !tree .get ("defaultInputModes" ).isArray ()) {
73- violations .add (
74- new RuleViolation ("'defaultInputModes' field must be an array" , "/defaultInputModes" ));
188+ if (skill .has ("tags" ) && !skill .get ("tags" ).isArray ()) {
189+ violations .add (new RuleViolation ("Skill 'tags' must be an array" , basePath + "/tags" ));
190+ } else if (skill .has ("tags" )) {
191+ validateStringArray (skill .get ("tags" ), basePath + "/tags" , "tag" , violations );
75192 }
76193
77- if (tree .has ("defaultOutputModes" ) && !tree .get ("defaultOutputModes" ).isArray ()) {
78- violations .add (new RuleViolation ("'defaultOutputModes' field must be an array" ,
79- "/defaultOutputModes" ));
194+ if (skill .has ("examples" ) && !skill .get ("examples" ).isArray ()) {
195+ violations .add (new RuleViolation ("Skill 'examples' must be an array" , basePath + "/examples" ));
196+ } else if (skill .has ("examples" )) {
197+ validateStringArray (skill .get ("examples" ), basePath + "/examples" , "example" , violations );
80198 }
81199
82- if (!violations .isEmpty ()) {
83- throw new RuleViolationException ("Invalid Agent Card" , RuleType .VALIDITY , level .name (),
84- violations );
200+ index ++;
201+ }
202+ }
203+
204+ private void validateArrayFields (JsonNode tree , Set <RuleViolation > violations ) {
205+ validateStringArrayField (tree , "defaultInputModes" , violations );
206+ validateStringArrayField (tree , "defaultOutputModes" , violations );
207+ }
208+
209+ private void validateStringArrayField (JsonNode tree , String fieldName , Set <RuleViolation > violations ) {
210+ if (!tree .has (fieldName )) {
211+ return ;
212+ }
213+
214+ JsonNode array = tree .get (fieldName );
215+ if (!array .isArray ()) {
216+ violations .add (new RuleViolation ("'" + fieldName + "' field must be an array" , "/" + fieldName ));
217+ return ;
218+ }
219+
220+ validateStringArray (array , "/" + fieldName , "item" , violations );
221+ }
222+
223+ private void validateStringArray (JsonNode array , String basePath , String itemName , Set <RuleViolation > violations ) {
224+ int index = 0 ;
225+ for (JsonNode item : array ) {
226+ if (!item .isTextual ()) {
227+ violations .add (new RuleViolation ("Each " + itemName + " must be a string" ,
228+ basePath + "/" + index ));
85229 }
230+ index ++;
231+ }
232+ }
86233
87- } catch (RuleViolationException e ) {
88- throw e ;
89- } catch (Exception e ) {
90- throw new RuleViolationException ("Invalid Agent Card JSON: " + e .getMessage (),
91- RuleType .VALIDITY , level .name (), e );
234+ private void validateAuthenticationField (JsonNode tree , Set <RuleViolation > violations ) {
235+ if (!tree .has ("authentication" )) {
236+ return ;
237+ }
238+
239+ JsonNode auth = tree .get ("authentication" );
240+ if (!auth .isObject ()) {
241+ violations .add (new RuleViolation ("'authentication' field must be an object" , "/authentication" ));
242+ return ;
243+ }
244+
245+ if (auth .has ("schemes" )) {
246+ if (!auth .get ("schemes" ).isArray ()) {
247+ violations .add (new RuleViolation ("'authentication.schemes' must be an array" ,
248+ "/authentication/schemes" ));
249+ } else {
250+ validateStringArray (auth .get ("schemes" ), "/authentication/schemes" , "scheme" , violations );
251+ }
252+ }
253+
254+ if (auth .has ("credentials" )) {
255+ if (!auth .get ("credentials" ).isArray ()) {
256+ violations .add (new RuleViolation ("'authentication.credentials' must be an array" ,
257+ "/authentication/credentials" ));
258+ } else {
259+ int index = 0 ;
260+ for (JsonNode cred : auth .get ("credentials" )) {
261+ if (!cred .isObject ()) {
262+ violations .add (new RuleViolation ("Credential at index " + index + " must be an object" ,
263+ "/authentication/credentials/" + index ));
264+ }
265+ index ++;
266+ }
267+ }
92268 }
93269 }
94270
95271 @ Override
96272 public void validateReferences (TypedContent content , List <ArtifactReference > references )
97273 throws RuleViolationException {
98- // Agent Cards don't support references in MVP
274+ // Agent Cards don't support references
99275 if (references != null && !references .isEmpty ()) {
100276 throw new RuleViolationException ("Agent Cards do not support references" ,
101277 RuleType .INTEGRITY , "NONE" ,
0 commit comments