Skip to content

❔ Question: Proposal for processing CustomData in OCPP 2.x. #384

@sderkacs

Description

@sderkacs

❔ 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions