ESPHome 2026.3.0-dev
Loading...
Searching...
No Matches
json_util.cpp
Go to the documentation of this file.
1#include "json_util.h"
2#include "esphome/core/log.h"
3
4// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
5
6namespace esphome {
7namespace json {
8
9static const char *const TAG = "json";
10
11#ifdef USE_PSRAM
12// Global allocator that outlives all JsonDocuments returned by parse_json()
13// This prevents dangling pointer issues when JsonDocuments are returned from functions
14// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - Must be mutable for ArduinoJson::Allocator
15static SpiRamAllocator global_json_allocator;
16#endif
17
19 // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
20 JsonBuilder builder;
21 JsonObject root = builder.root();
22 f(root);
23 return builder.serialize();
24 // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
25}
26
27bool parse_json(const std::string &data, const json_parse_t &f) {
28 // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
29 return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size(), f);
30}
31
32bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
33 // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
34 JsonDocument doc = parse_json(data, len);
35 if (doc.overflowed() || doc.isNull())
36 return false;
37 return f(doc.as<JsonObject>());
38 // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
39}
40
41JsonDocument parse_json(const uint8_t *data, size_t len) {
42 // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
43 if (data == nullptr || len == 0) {
44 ESP_LOGE(TAG, "No data to parse");
45 return JsonObject(); // return unbound object
46 }
47#ifdef USE_PSRAM
48 JsonDocument json_document(&global_json_allocator);
49#else
50 JsonDocument json_document;
51#endif
52 if (json_document.overflowed()) {
53 ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
54 return JsonObject(); // return unbound object
55 }
56 DeserializationError err = deserializeJson(json_document, data, len);
57
58 if (err == DeserializationError::Ok) {
59 return json_document;
60 } else if (err == DeserializationError::NoMemory) {
61 ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
62 return JsonObject(); // return unbound object
63 }
64 ESP_LOGE(TAG, "Parse error: %s", err.c_str());
65 return JsonObject(); // return unbound object
66 // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
67}
68
70 // ===========================================================================================
71 // CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING
72 // ===========================================================================================
73 //
74 // This function is carefully structured to enable NRVO. The compiler constructs `result`
75 // directly in the caller's stack frame, eliminating the move constructor call entirely.
76 //
77 // WITHOUT NRVO: Each return would trigger SerializationBuffer's move constructor, which
78 // must memcpy up to 512 bytes of stack buffer content. This happens on EVERY JSON
79 // serialization (sensor updates, web server responses, MQTT publishes, etc.).
80 //
81 // WITH NRVO: Zero memcpy, zero move constructor overhead. The buffer lives directly
82 // where the caller needs it.
83 //
84 // Requirements for NRVO to work:
85 // 1. Single named variable (`result`) returned from ALL paths
86 // 2. All paths must return the SAME variable (not different variables)
87 // 3. No std::move() on the return statement
88 //
89 // If you must modify this function:
90 // - Keep a single `result` variable declared at the top
91 // - All code paths must return `result` (not a different variable)
92 // - Verify NRVO still works by checking the disassembly for move constructor calls
93 // - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer"
94 // Should show only destructor, NOT move constructor
95 //
96 // Try stack buffer first. 640 bytes covers 99.9% of JSON payloads (sensors ~200B,
97 // lights ~170B, climate ~500-700B). Only entities with 40+ options exceed this.
98 //
99 // IMPORTANT: ArduinoJson's serializeJson() with a bounded buffer returns the actual
100 // bytes written (truncated count), NOT the would-be size like snprintf(). When the
101 // payload exceeds the buffer, the return value equals the buffer capacity. The heap
102 // fallback doubles the buffer size until the payload fits. This avoids instantiating
103 // measureJson()'s DummyWriter templates (~736 bytes flash) at the cost of temporarily
104 // over-allocating heap (at most 2x) for the rare payloads that exceed 640 bytes.
105 //
106 // ===========================================================================================
107 constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE;
108 SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null)
109
110 if (doc_.overflowed()) {
111 ESP_LOGE(TAG, "JSON document overflow");
112 auto *buf = result.data_writable_();
113 buf[0] = '{';
114 buf[1] = '}';
115 buf[2] = '\0';
116 result.set_size_(2);
117 return result;
118 }
119
120 size_t size = serializeJson(doc_, result.data_writable_(), buf_size);
121 if (size < buf_size) {
122 // Fits in stack buffer - update size to actual length
123 result.set_size_(size);
124 return result;
125 }
126
127 // Payload exceeded stack buffer. Double the buffer and retry until it fits.
128 // In practice, one iteration (1024 bytes) covers all known entity types.
129 // Payloads exceeding 1024 bytes are not known to exist in real configurations.
130 // Cap at 5120 as a safety limit to prevent runaway allocation.
131 constexpr size_t max_heap_size = 5120;
132 size_t heap_size = buf_size * 2;
133 while (heap_size <= max_heap_size) {
134 result.reallocate_heap_(heap_size - 1);
135 size = serializeJson(doc_, result.data_writable_(), heap_size);
136 if (size < heap_size) {
137 result.set_size_(size);
138 return result;
139 }
140 heap_size *= 2;
141 }
142 // Payload exceeds 5120 bytes - return truncated result
143 ESP_LOGW(TAG, "JSON payload too large, truncated to %zu bytes", size);
144 result.set_size_(size);
145 return result;
146}
147
148} // namespace json
149} // namespace esphome
Builder class for creating JSON documents without lambdas.
Definition json_util.h:170
SerializationBuffer serialize()
Serialize the JSON document to a SerializationBuffer (stack-first allocation) Uses 512-byte stack buf...
Definition json_util.cpp:69
Buffer for JSON serialization that uses stack allocation for small payloads.
Definition json_util.h:22
std::function< void(JsonObject)> json_build_t
Callback function typedef for building JsonObjects.
Definition json_util.h:151
bool parse_json(const std::string &data, const json_parse_t &f)
Parse a JSON string and run the provided json parse function if it's valid.
Definition json_util.cpp:27
std::function< bool(JsonObject)> json_parse_t
Callback function typedef for parsing JsonObjects.
Definition json_util.h:148
SerializationBuffer build_json(const json_build_t &f)
Build a JSON string with the provided json build function.
Definition json_util.cpp:18
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string size_t len
Definition helpers.h:817
size_t size
Definition helpers.h:854