-
Notifications
You must be signed in to change notification settings - Fork 161
Description
❔ What is your question?
Sorry for reopening this topic.
I read the previous discussion, but I feel like there's some misunderstanding. There's no need to "catch" custom fields directly in requests/responses. The JSON schema for OCPP (both 2.0 and 2.1) defines CustomData as a separate type: CustomDataType.
"CustomDataType": {
"description": "This class does not get 'AdditionalProperties = false' in the schema generation, so it can be extended with arbitrary JSON properties to allow adding custom data.",
"javaType": "CustomData",
"type": "object",
"properties": {
"vendorId": {
"type": "string",
"maxLength": 255
}
},
"required": [
"vendorId"
]
}
Accordingly, other data types that can contain custom data should have an optional "customData" field. (e.g. AuthorizeRequest)
{
"$schema": "http://json-schema.org/draft-06/schema#",
"$id": "urn:OCPP:Cp:2:2020:3:AuthorizeRequest",
"comment": "OCPP 2.0.1 FINAL",
"definitions": {
// ...
},
"type": "object",
"additionalProperties": false,
"properties": {
"customData": {
"$ref": "#/definitions/CustomDataType"
},
"idToken": {
"$ref": "#/definitions/IdTokenType"
},
"certificate": {
"description": "The X.509 certificated presented by EV and encoded in PEM format.\r\n",
"type": "string",
"maxLength": 5500
},
"iso15118CertificateHashData": {
"type": "array",
"additionalItems": false,
"items": {
"$ref": "#/definitions/OCSPRequestDataType"
},
"minItems": 1,
"maxItems": 4
}
},
"required": [
"idToken"
]
}
Example of custom data from the OCPP specification (OCPP 2.0.1 Part 4, Chapter 9):
{
"customData": {
"vendorId": "com.mycompany.customheartbeat",
"mainMeterValue": 12345,
"sessionsToDate": 342
}
}
Therefore, special handling (custom marshaler and unmarshaler) should only be implemented for CustomDataType.
Possible implementation of CustomDataType:
type CustomData struct {
VendorId string `json:"vendorId" validate:"required,max=255"`
Values map[string]any `json:"-"` // Ignore during default JSON unmarshaling
}
func (c *CustomData) UnmarshalJSON(data []byte) error {
raw := make(map[string]interface{})
// Unmarshal into raw map
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
// Do not return any error here. Will fail later during validation.
rawVendorId := raw["vendorId"]
vendorId := ""
if rawVendorId != nil {
s, ok := rawVendorId.(string)
if ok {
vendorId = s
}
}
*c = CustomData{
VendorId: vendorId,
Values: raw,
}
// Remove vendorId from Values to avoid duplication
delete(c.Values, "vendorId")
return nil
}
func (c CustomData) MarshalJSON() ([]byte, error) {
output := make(map[string]interface{})
output["vendorId"] = c.VendorId
for k, v := range c.Values {
output[k] = v
}
return json.Marshal(output)
}
Modified AuthorizeRequest:
// The field definition of the Authorize request payload sent by the Charging Station to the CSMS.
type AuthorizeRequest struct {
Certificate string `json:"certificate,omitempty" validate:"max=5500"`
IdToken types.IdToken `json:"idToken" validate:"required"`
CertificateHashData []types.OCSPRequestDataType `json:"iso15118CertificateHashData,omitempty" validate:"max=4,dive"`
CustomData *types.CustomData `json:"customData,omitempty" validate:"omitempty"`
}
I tested this approach on examples from the project, and everything works perfectly. This change also doesn't break the tests.
Example of a validation error with an empty vendorId:
INFO[2025-10-07T00:19:50+03:00] connected to CSMS at ws://localhost:8887
INFO[2025-10-07T00:19:50+03:00] dispatched request 1363214786 to server
INFO[2025-10-07T00:19:50+03:00] status: Accepted, interval: 600, current time: 2025-10-06 21:19:50 +0000 UTC message=BootNotification
INFO[2025-10-07T00:19:50+03:00] operational status for evse 1 updated to: Operative
INFO[2025-10-07T00:19:50+03:00] dispatched request 3437763191 to server
INFO[2025-10-07T00:19:50+03:00] status for evse 1 - connector 0 updated to: Available message=StatusNotification
INFO[2025-10-07T00:19:52+03:00] reservation 42 accepted for evse 1, connector 0 message=ReserveNow
INFO[2025-10-07T00:19:52+03:00] dispatched request 1561461413 to server
INFO[2025-10-07T00:19:52+03:00] status for evse 1 - connector 0 updated to: Reserved message=StatusNotification
INFO[2025-10-07T00:19:53+03:00] reservation 42 for evse 1 canceled message=CancelReservation
INFO[2025-10-07T00:19:53+03:00] dispatched request 1968302455 to server
INFO[2025-10-07T00:19:53+03:00] status for evse 1 - connector 0 updated to: Available message=StatusNotification
INFO[2025-10-07T00:19:55+03:00] dispatched request 1681938932 to server
INFO[2025-10-07T00:19:55+03:00] status for evse 1 - connector 0 updated to: Occupied message=StatusNotification
INFO[2025-10-07T00:19:55+03:00] dispatched request 4150724469 to server
INFO[2025-10-07T00:19:55+03:00] transaction 3DAA0EE9-4A49-62BF-954F-7779A5C1E666 started message=TransactionEvent
INFO[2025-10-07T00:19:55+03:00] dispatched request 2999641483 to server
FATA[2025-10-07T00:19:55+03:00] ocpp message (2999641483): OccurrenceConstraintViolation - Field CallResult.Payload.CustomData.VendorId required but not found for feature Authorize
I also looked at other OCPP implementations (libocpp, ChargeTimeEU), and everything is the same.
If there is interest in this and this solution suits everyone, I can prepare a feature request for all types that can have custom data.
Which OCPP version referring to?
- OCPP 1.6
- OCPP 2.0.1
Are you using any OCPP extensions?
- OCPP 1.6 Security
- OCPP 1.6 Plug and Charge
👀 Have you spent some time to check if this question has been asked before?
- I checked and didn't find a similar issue