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