Skip to content

Commit aa2c550

Browse files
committed
fix: improve JSON parsing robustness and handle optional Order fields
- Add field existence checks to parsing macros (TIMESTAMP_FROM_JSON, etc.) - Fix Order deserialization for optional fields (edit_history, attached_order_configuration) - Support both creation_time and created_time field names - Enhance error logging with contextual exception details - Bump version to 0.2.1
1 parent d06089c commit aa2c550

6 files changed

Lines changed: 52 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.1] - 2026-02-19
9+
10+
### Changed
11+
- **BREAKING:** Renamed WebSocket channel `HEARTBEAT` to `HEARTBEATS` to match Coinbase API specification
12+
- Improved JSON parsing macros to check for field existence before parsing (`TIMESTAMP_FROM_JSON`, `NANOSECONDS_FROM_JSON`, `DOUBLE_FROM_JSON`, `INT_FROM_JSON`)
13+
- Enhanced error logging to combine context and error messages for better debugging
14+
15+
### Fixed
16+
- Fixed Order JSON parsing to handle optional fields (`edit_history`, `creation_time`, `current_pending_replace`, `attached_order_configuration`)
17+
- Fixed Order parsing to support both `creation_time` and `created_time` field names
18+
- Fixed WebSocket error handling to properly process and dispatch error messages instead of throwing exceptions
19+
- Fixed user event processing to use correct JSON path for order updates (`event.at("orders")` instead of `j.at("orders")`)
20+
- Fixed sequence number checks to handle messages without `sequence_num` field
21+
- Improved null-safety in JSON parsing throughout order and websocket modules
22+
823
## [0.2.0] - 2026-02-19
924

1025
### Added

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.20)
33
set(CMAKE_CXX_STANDARD 20)
44

55
project(coinbase-advanced-cpp
6-
VERSION 0.2.0
6+
VERSION 0.2.1
77
LANGUAGES CXX)
88

99
if (CMAKE_BUILD_TYPE MATCHES Debug)

include/coinbase/order.hpp

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -522,10 +522,13 @@ inline void from_json(const json &j, Order &o) {
522522
VARIABLE_FROM_JSON(j, o, retail_portfolio_id);
523523
VARIABLE_FROM_JSON(j, o, originating_order_id);
524524
VARIABLE_FROM_JSON(j, o, attached_order_id);
525-
if (!j["current_pending_replace"].is_null()) {
526-
o.current_pending_replace = j["current_pending_replace"];
525+
STRUCT_FROM_JSON(j, o, current_pending_replace);
526+
if (j.contains("creation_time")) {
527+
o.created_time = milliseconds_from_json(j, "creation_time");
528+
}
529+
else {
530+
TIMESTAMP_FROM_JSON(j, o, created_time);
527531
}
528-
TIMESTAMP_FROM_JSON(j, o, created_time);
529532
TIMESTAMP_FROM_JSON(j, o, last_fill_time);
530533
DOUBLE_FROM_JSON(j, o, completion_percentage);
531534
DOUBLE_FROM_JSON(j, o, fee);
@@ -537,7 +540,9 @@ inline void from_json(const json &j, Order &o) {
537540
DOUBLE_FROM_JSON(j, o, workable_size);
538541
DOUBLE_FROM_JSON(j, o, workable_size_completion_pct);
539542
INT_FROM_JSON(j, o, number_of_fills);
540-
o.edit_history = j["edit_history"];
543+
if (j.contains("edit_history")) {
544+
o.edit_history = j["edit_history"];
545+
}
541546
ENUM_FROM_JSON(j, o, side);
542547
o.status = to_order_status(j.at("status").get<std::string_view>());
543548
ENUM_FROM_JSON(j, o, contract_expiry_type);
@@ -549,10 +554,8 @@ inline void from_json(const json &j, Order &o) {
549554
VARIABLE_FROM_JSON(j, o, size_inclusive_of_fees);
550555
VARIABLE_FROM_JSON(j, o, settled);
551556
VARIABLE_FROM_JSON(j, o, is_liquidation);
552-
o.order_configuration = j["order_configuration"];
553-
if (j.contains("attached_order_configuration")) {
554-
o.attached_order_configuration = j["attached_order_configuration"];
555-
}
557+
STRUCT_FROM_JSON(j, o, order_configuration);
558+
STRUCT_FROM_JSON(j, o, attached_order_configuration);
556559
}
557560
catch (const std::exception &e) {
558561
LOG_INFO(j.dump().c_str());

include/coinbase/utils.hpp

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ inline double double_from_json(const json &j, std::string_view field) {
5050
}
5151
catch(const std::exception& e)
5252
{
53-
LOG_INFO("{} {}", field, j.dump());
54-
LOG_ERROR(e.what());
53+
LOG_ERROR("double_from_json exception: {}. field: {} {}", e.what(), field, j.dump());
5554
}
5655
return 0.;
5756
}
@@ -67,8 +66,7 @@ inline int32_t int_from_json(const json &j, std::string_view field) {
6766
}
6867
catch(const std::exception& e)
6968
{
70-
LOG_INFO("{} {}", field, j.dump());
71-
LOG_ERROR(e.what());
69+
LOG_ERROR("int_from_json exception: {}. field: {} {}", e.what(), field, j.dump());
7270
}
7371
return 0;
7472
}
@@ -110,10 +108,10 @@ inline std::string to_string(double value, Side side, double min_increment) {
110108
return oss.str();
111109
}
112110

113-
#define TIMESTAMP_FROM_JSON(j, o, field) o.field = milliseconds_from_json(j, #field)
114-
#define NANOSECONDS_FROM_JSON(j, o, field) o.field = nanoseconds_from_json(j, #field)
115-
#define DOUBLE_FROM_JSON(j, o, field) o.field = double_from_json(j, #field)
116-
#define INT_FROM_JSON(j, o, field) o.field = int_from_json(j, #field)
111+
#define TIMESTAMP_FROM_JSON(j, o, field) if (j.contains(#field)) o.field = milliseconds_from_json(j, #field)
112+
#define NANOSECONDS_FROM_JSON(j, o, field) if (j.contains(#field)) o.field = nanoseconds_from_json(j, #field)
113+
#define DOUBLE_FROM_JSON(j, o, field) if (j.contains(#field)) o.field = double_from_json(j, #field)
114+
#define INT_FROM_JSON(j, o, field) if (j.contains(#field)) o.field = int_from_json(j, #field)
117115
#define VARIABLE_FROM_JSON(j, o, field) if (j.contains(#field) && !j[#field].is_null()) try { j.at(#field).get_to(o.field); } catch(const std::exception &e) { LOG_INFO(#field " {}", j.dump()); LOG_ERROR(e.what()); }
118116
#define BOOL_FROM_JSON(j, o, field) if (j.contains(#field) && !j[#field].is_null() && j[#field].is_string()) { if (j[#field] == "true") o.field = true; else if (j[#field] == "false") o.field = false; } else try { j.at(#field).get_to(o.field); } catch(const std::exception &e) { LOG_INFO(#field " {}", j.dump()); LOG_ERROR(e.what()); }
119117
#define BOOL_FROM_JSON_VALUE(j, o, field, j_name) if (j.contains(j_name) && !j[j_name].is_null() && j[j_name].is_string()) { if (j[j_name] == "true") o.field = true; else if (j[j_name] == "false") o.field = false; } else try { j.at(j_name).get_to(o.field); } catch(const std::exception &e) { LOG_INFO(j_name " {}", j.dump()); LOG_ERROR(e.what()); }

include/coinbase/websocket.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ using Websocket = slick::net::Websocket;
2929
using json = nlohmann::json;
3030

3131
enum WebSocketChannel : uint8_t {
32-
HEARTBEAT,
32+
HEARTBEATS,
3333
LEVEL2,
3434
MARKET_TRADES,
3535
TICKER,

src/websocket.cpp

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ namespace coinbase {
88

99
std::string to_string(WebSocketChannel channel) {
1010
switch(channel) {
11-
case WebSocketChannel::HEARTBEAT:
12-
return "heartbeat";
11+
case WebSocketChannel::HEARTBEATS:
12+
return "heartbeats";
1313
case WebSocketChannel::LEVEL2:
1414
return "level2";
1515
case WebSocketChannel::MARKET_TRADES:
@@ -279,7 +279,7 @@ void WebSocketClient::unsubscribe(const std::vector<std::string> &product_ids, c
279279
if (websocket->status() <= Websocket::Status::CONNECTED) {
280280
websocket->send(unsubscribe_str.c_str(), unsubscribe_str.size());
281281
}
282-
if (channel == WebSocketChannel::HEARTBEAT) {
282+
if (channel == WebSocketChannel::HEARTBEATS) {
283283
if (user_data_websocket_ && user_data_websocket_->status() <= Websocket::Status::CONNECTED) {
284284
user_data_websocket_->send(unsubscribe_str.c_str(), unsubscribe_str.size());
285285
}
@@ -445,7 +445,13 @@ void WebSocketClient::onUserDataError(std::string &&err) {
445445
void DataHandler::processMarketData(WebSocketClient *ws_client, const char* data, std::size_t size) {
446446
try {
447447
auto j = json::parse(data, data + size);
448-
checkMarketDataSequenceNumber(ws_client, j["sequence_num"]);
448+
if (j.contains("sequence_num")) {
449+
checkMarketDataSequenceNumber(ws_client, j["sequence_num"]);
450+
}
451+
if (j["type"] == "error") {
452+
callbacks_->onMarketDataError(ws_client, j["message"]);
453+
return;
454+
}
449455
auto channel = j["channel"];
450456
if (channel == "l2_data") {
451457
processLevel2Update(ws_client, j);
@@ -479,7 +485,13 @@ void DataHandler::processMarketData(WebSocketClient *ws_client, const char* data
479485
void DataHandler::processUserData(WebSocketClient *ws_client, const char* data, std::size_t size) {
480486
try {
481487
auto j = json::parse(data, data + size);
482-
checkUserDataSequenceNumber(ws_client, j["sequence_num"]);
488+
if (j.contains("sequence_num")) {
489+
checkUserDataSequenceNumber(ws_client, j["sequence_num"]);
490+
}
491+
if (j["type"] == "error") {
492+
callbacks_->onUserDataError(ws_client, j["message"]);
493+
return;
494+
}
483495
auto channel = j["channel"];
484496
if (channel == "user") {
485497
processUserEvent(ws_client, j);
@@ -585,7 +597,7 @@ void DataHandler::processUserEvent(WebSocketClient *ws_client, const json &j) {
585597
callbacks_->onUserDataSnapshot(ws_client, j.at("sequence_num").get<uint64_t>(), orders, positions.at("perpetual_futures_positions"), positions.at("expiring_futures_positions"));
586598
}
587599
else if (event["type"] == "update") {
588-
callbacks_->onOrderUpdates(ws_client, j.at("sequence_num").get<uint64_t>(), j.at("orders"));
600+
callbacks_->onOrderUpdates(ws_client, j.at("sequence_num").get<uint64_t>(), event.at("orders"));
589601
}
590602
else {
591603
LOG_WARN("unknown user event type: {}", j["type"].get<std::string_view>());

0 commit comments

Comments
 (0)