@@ -22,14 +22,17 @@ class JsonSchemaValidator
2222 public static function validate (Call |CallResult |CallError |array $ message , string $ version , bool $ throwException = true ): bool |CallError
2323 {
2424 // CallError messages represent protocol-level error responses (e.g., NotImplemented).
25- // They have no payload to validate — the error code itself IS the message.
25+ // They have no JSON schema to validate against — instead we verify they parse
26+ // into a well-formed CallError with a recognized error structure.
2627 if ($ message instanceof CallError) {
27- return true ;
28+ return self :: validateCallErrorStructure ( $ message , $ throwException ) ;
2829 }
2930
30- // Raw array with messageTypeID=4 is also a CallError — skip validation.
31+ // Raw array with messageTypeID=4 is a CallError. Parse it first so we validate
32+ // its structure rather than blindly returning true.
3133 if (is_array ($ message ) && ($ message ['messageTypeID ' ] ?? $ message [0 ] ?? null ) === 4 ) {
32- return true ;
34+ $ callError = self ::normalizeCallErrorArray ($ message );
35+ return self ::validateCallErrorStructure ($ callError , $ throwException );
3336 }
3437
3538 $ registry = new JsonSchemaRegistry ();
@@ -281,4 +284,79 @@ private static function resolveSchemaReference(string $reference, array $rootSch
281284
282285 return is_array ($ current ) ? $ current : [];
283286 }
287+
288+ /**
289+ * Validate that a CallError has a well-formed structure.
290+ *
291+ * CallError messages don't have a JSON schema to validate against, but we
292+ * verify basic structural integrity: a non-empty errorCode, a string
293+ * errorDescription, and an array errorDetails.
294+ */
295+ private static function validateCallErrorStructure (CallError $ callError , bool $ throwException ): bool |CallError
296+ {
297+ $ errors = [];
298+
299+ if (!is_string ($ callError ->errorCode ) || $ callError ->errorCode === '' ) {
300+ $ errors [] = 'The property errorCode is required and must be a non-empty string ' ;
301+ }
302+
303+ if (isset ($ callError ->errorDescription ) && !is_string ($ callError ->errorDescription )) {
304+ $ errors [] = 'The property errorDescription must be a string ' ;
305+ }
306+
307+ if (!is_array ($ callError ->errorDetails )) {
308+ $ errors [] = 'The property errorDetails must be an array ' ;
309+ }
310+
311+ if ($ errors !== []) {
312+ $ protocolError = new ProtocolError (
313+ $ callError ->uniqueId ,
314+ null ,
315+ null ,
316+ $ errors
317+ );
318+
319+ if ($ throwException ) {
320+ throw new \Exception (self ::formatValidationExceptionMessage ($ protocolError ));
321+ }
322+
323+ return $ protocolError ;
324+ }
325+
326+ return true ;
327+ }
328+
329+ /**
330+ * Normalize a raw CallError array into a CallError object.
331+ *
332+ * Handles both the raw OCPP wire format [4, uniqueId, errorCode, ...]
333+ * and the parsed message format ['messageTypeID' => 4, ...].
334+ */
335+ private static function normalizeCallErrorArray (array $ message ): CallError
336+ {
337+ // Raw OCPP wire format: [4, uniqueId, errorCode, errorDescription, errorDetails]
338+ if (isset ($ message [0 ]) && $ message [0 ] === 4 ) {
339+ return CallError::fromArray ($ message );
340+ }
341+
342+ // Parsed message format from parseJsonMessage:
343+ // ['messageTypeID' => 4, 'uniqueId' => '...', 'action' => '...', 'payload' => [...]]
344+ $ uniqueId = $ message ['uniqueId ' ] ?? '' ;
345+ $ errorCode = $ message ['action ' ] ?? $ message ['errorCode ' ] ?? '' ;
346+ $ payload = $ message ['payload ' ] ?? [];
347+
348+ // The payload in parsed format may contain errorDescription and errorDetails
349+ $ errorDescription = '' ;
350+ $ errorDetails = [];
351+
352+ if (is_array ($ payload )) {
353+ $ errorDescription = $ payload ['errorDescription ' ] ?? '' ;
354+ $ errorDetails = $ payload ['errorDetails ' ] ?? [];
355+ }
356+
357+ // Rebuild into raw format for CallError::fromArray
358+ $ raw = [4 , $ uniqueId , $ errorCode , $ errorDescription , $ errorDetails ];
359+
360+ return CallError::fromArray ($ raw );
361+ }
284362}
0 commit comments