A strongly typed Rust implementation of the Open Charge Point Protocol (OCPP) message model.
ocpp-types provides the request/response payload types for OCPP 1.6J, 2.0.1, and 2.1, generated
from the official JSON schemas. It's no_std and, by default, allocation-free: field sizes are
bounded at the type level with heapless collections, sized to the
limits stated in each version's specification.
The crate is designed to be lightweight and reusable across embedded firmware, Linux-based charge points, simulators, CSMS implementations, and tooling.
| Version | Status |
|---|---|
| OCPP 1.6J | ✅ Available |
| OCPP 2.0.1 | ✅ Available |
| OCPP 2.1 | ✅ Available |
Each version lives in its own module (v16, v201, v21), since the same message name can
differ in shape across versions.
use ocpp_types::Action;
use ocpp_types::v16::{BootNotificationRequest, IdTag};
let request = BootNotificationRequest {
charge_point_vendor: "Flowion".try_into()?,
charge_point_model: "Simulator".try_into()?,
charge_box_serial_number: None,
charge_point_serial_number: None,
firmware_version: None,
iccid: None,
imsi: None,
meter_serial_number: None,
meter_type: None,
};
assert_eq!(BootNotificationRequest::ACTION, "BootNotification");Every field that OCPP bounds with a maxLength/maxItems is sized exactly to that bound (a
heapless::String<20>, not an arbitrary String), so a value that doesn't fit fails at
construction (try_into()/try_from()), not somewhere downstream at serialization time.
crates/ocpp-types/examples/ has complete, runnable programs for
the topics on this page:
cargo run -p ocpp-types --example basic_usage
cargo run -p ocpp-types --example serialization --features serde
cargo run -p ocpp-types --example rpc_error_codes
cargo run -p ocpp-types --example unbounded_fields # const-generic capacity
cargo run -p ocpp-types --example unbounded_fields --features alloc # alloc collections instead
cargo run -p ocpp-types --example envelope --features serde
cargo run -p ocpp-types --example validate --features validateWith the serde feature enabled, every message implements Serialize/Deserialize, and the
[Action] trait gains zero-allocation JSON helpers backed by
serde-json-core — the caller owns the buffer, nothing is
heap-allocated:
use ocpp_types::Action;
let mut buf = [0u8; 256];
let json: &str = request.to_json_str(&mut buf)?;
let parsed = BootNotificationRequest::from_json_str(json)?;to_json_slice/from_json_slice are also available for working with raw bytes instead of &str.
A handful of fields (free-text strings, a few arrays) have no maxLength/maxItems in the spec,
so there's no size to give a heapless collection without guessing one. These expose a const
generic parameter the caller can pick, defaulting to a reasonable size (1024 for strings, 16 for
arrays) so most code never has to think about it:
use ocpp_types::v16::HeartbeatResponse;
// Uses the default capacity:
let response: HeartbeatResponse = HeartbeatResponse {
current_time: heapless::String::try_from("2024-01-01T00:00:00Z")?,
};
// Or pick a smaller one explicitly:
let response: HeartbeatResponse<64> = HeartbeatResponse {
current_time: heapless::String::try_from("2024-01-01T00:00:00Z")?,
};With the alloc feature enabled instead, these fields become plain alloc::string::String /
alloc::vec::Vec<T>, and the const generic disappears entirely — useful on targets with a real
allocator (a CSMS backend, a simulator) that would rather not pick a bound at all.
Most of the specification is already in the types: a property the schema bounds at
maxLength: 20 is a heapless::String<20>, so an over-long value cannot be constructed, let alone
sent. Two categories escape that, and the validate feature covers them:
- Bounds too large to store inline. A string bounded above 512 characters, or an array above 16
elements, is not held at the spec's ceiling — that would make every message containing it
enormous by value. Under
allocthose fields are a growableString/Vecwith no bound at all; withoutallocthey areheaplesscollections at a capacity you pick, which may sit either side of the spec's.AuthorizeRequest::certificateis one:maxLength: 5500in the schema, a plainStringin the default build. - Constraints no collection type expresses.
minItems(usually1, i.e. "must not be empty"),minimum/maximumon numbers, and 1.6J'smultipleOfon charging limits.
use ocpp_types::validate::{Validate, ValidationErrorKind};
request.certificate = Some("-".repeat(6000));
let error = request.validate().unwrap_err();
assert_eq!(error.kind(), ValidationErrorKind::TooLong { len: 6000, max: 5500 });
assert_eq!(error.to_string(), "certificate: expected at most 5500 characters, got 6000");Errors report the JSON path to the value, through nested structs and array indices
(csChargingProfiles.chargingSchedule.chargingSchedulePeriod[1].limit), and classify as either a
property or an occurrence constraint — the two CALLERROR codes OCPP answers these with.
This is worth reaching for on the sending side, and most of all in a CSMS, where the unbounded
fields live: an over-long payload otherwise comes back as a CALLERROR that names no field.
Nothing calls it for you — validation never runs on the serialize path.
2.0.1 and 2.1's DataTransfer carries a data field the specification gives no type at all —
"open to implementation", agreed between the two parties. There's no single Rust type for arbitrary
JSON without an allocator, so the payload type is yours to pick, as a type parameter defaulting to
() ("this deployment sends no data"):
use ocpp_types::v201::DataTransferRequest;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
struct VendorPayload {
session_id: u32,
}
let request: DataTransferRequest<VendorPayload> = DataTransferRequest {
custom_data: None,
data: Some(VendorPayload { session_id: 42 }),
message_id: None,
vendor_id: heapless::String::try_from("com.example")?,
};1.6J's DataTransfer.data is a plain string in that version's schema, so it stays
Option<heapless::String<N>> and needs no parameter.
Each version also exposes an RpcErrorCode enum covering the CALLERROR codes defined by that
version's OCPP-J specification, implementing core::error::Error. These aren't identical across
versions — 2.0.1/2.1 renamed FormationViolation to FormatViolation, fixed a spelling error in
Occur(r)enceConstraintViolation, and added two new codes — so each version's is its own type,
not shared:
use ocpp_types::v16::RpcErrorCode;
let error = RpcErrorCode::NotImplemented;
println!("{error}"); // "Requested Action is not known by receiver"With the serde feature, Call/CallResult/CallError model the OCPP-J array-based envelope
every message travels in — generic over the payload type, so there's one definition covering every
version rather than one per version:
use ocpp_types::v16::{AuthorizeRequest, IdTag};
use ocpp_types::{Call, MessageId};
let call = Call {
message_id: MessageId::try_from("19223201")?,
payload: AuthorizeRequest {
id_tag: IdTag::try_from("ABC123")?,
},
};
let mut buf = [0u8; 256];
let len = serde_json_core::to_slice(&call, &mut buf)?;
// [2,"19223201","Authorize",{"idTag":"ABC123"}]The wire's "Action" string comes from AuthorizeRequest::ACTION, not a redundant stored field —
and is validated against it when parsing a Call<T> back, so a Call<AuthorizeRequest> you get out
really is one. CallResultError/SendMessage cover OCPP 2.1's additional CALLRESULTERROR/SEND
message types. See crates/ocpp-types/examples/envelope.rs.
| Feature | Default | Effect |
|---|---|---|
serde |
off | Serialize/Deserialize on every type, plus [Action]'s JSON helpers. |
alloc |
on | Fields with no spec-given bound become alloc collections instead of const-generic heapless ones. Combine with serde to also serialize them. |
validate |
off | Validate on every message: checks the schema constraints the types can't carry (see Validating a payload). |
Without any features, the crate has exactly one dependency: heapless.
- OCPP request/response types for 1.6J, 2.0.1, and 2.1
- Common/shared data structures and enums, deduplicated per version
serdeserialization, including OCPP-JCALLERRORerror codes and theCALL/CALLRESULT/CALLERROR(and 2.1'sCALLRESULTERROR/SEND) WebSocket envelopes- Doc comments carried over from the spec's own field/message descriptions
This crate intentionally does not implement:
- The actual WebSocket transport (opening/maintaining the connection, framing, reconnects)
- Message routing, charge point state machines, or CSMS logic
- Smart charging algorithms
Those responsibilities belong in higher-level crates built on top of this one.
ocpp-types is intended as the foundation for a broader Rust OCPP ecosystem — the shared data
model that other crates build transport, state machines, and application logic on top of.
ocpp-types
│
┌───────────┴───────────┐
│ │
ocpp-transport ocpp-charge-point
│ │
└───────────┬───────────┘
│
Applications
| Crate | Purpose | Status |
|---|---|---|
ocpp-types |
Protocol data model | ✅ This crate |
ocpp-transport |
Message framing and transport abstractions | 📋 Planned |
ocpp-charge-point |
Charge Point implementation | 📋 Planned |
charge-point-simulator |
OCPP testing and simulation tools | 📋 Planned |
- ocpp-charge-point — a reusable Rust implementation of an OCPP Charge Point.
- rust-ocpp — shared Rust libraries for the Open Charge Point Protocol.
Nothing in ocpp-types is hand-written except a handful of primitives (IdTag, RpcErrorCode)
that have no equivalent in the JSON schemas. Everything else is generated by ocpp-codegen (a
workspace-internal, unpublished dev tool) from the schemas in schemas/. If a type looks wrong,
the fix belongs in the generator, not in a hand-edit of the generated file — every generated file
says as much at the top, and regenerating overwrites hand-edits anyway.
./scripts/generate.shCI regenerates and diffs on every push, so the committed output can't silently drift from what the schemas and generator would actually produce.
- Transport agnostic. No knowledge of WebSockets or networking — this crate just models the protocol's data shapes.
no_stdby default. Works on embedded microcontrollers, Linux-based charge points, cloud services, and desktop simulators alike;allocis opt-in, never required.- Specification-first. Field names, bounds, and doc comments come directly from the OCPP JSON schemas and OCPP-J specifications, not reinterpreted by hand.
Licensed under either of
at your option.
Contributions are welcome — implementing new message types, fixing specification inconsistencies, adding tests, or improving documentation. See CONTRIBUTING.md for the workflow, since types are generated rather than hand-written. Participation is governed by our Code of Conduct. Found a security issue? See SECURITY.md.