Ultra-low-RAM, zero-copy JSON parser for AVR microcontrollers.
Designed for ATmega devices parsing GSM server responses. A JSON instance occupies 7 bytes of RAM regardless of the payload size β no heap allocation, no internal buffer copy, no dynamic memory.
- Zero-copy β stores only a pointer to your buffer; the original string is never duplicated
- No heap β every instance is 7 bytes on the stack;
getObject()child instances are also 7 bytes each - PROGMEM keys β all key lookups use
F("key"), keeping key strings in Flash rather than RAM - Single-pass validation β locate and validate the JSON block in one scan (no double pass, no duplicate CPI instructions on AVR)
- Top-level key isolation β nested objects with identically-named keys never shadow each other
- GSM prefix handling β
AT+CMQTTRXPAYLOADand similar headers before{are skipped automatically - Full type coverage β
int32_t,float,bool,string, nestedobject,array - Pay-as-you-go Flash β float library (~1 880 B) is only linked when
getFloat()is called
| Scenario | Our RAM | ArduinoJson RAM | Our Flash | ArduinoJson Flash |
|---|---|---|---|---|
| Flat integers | 231 B | 330 B | 2 674 B | 8 880 B |
| Bool + string | 247 B | 354 B | 2 762 B | 9 086 B |
| GSM prefix + string | 243 B | 342 B | 2 908 B | 9 070 B |
| Float + array + typeOf | 295 B | 388 B | 5 308 B | 10 060 B |
| Heavy server config (19 keys + nested) | 529 B | 740 B | 5 710 B | 11 590 B |
ArduinoJson static RAM shown; at runtime
JsonDocumentadds ~200β600 B from the heap (not reflected above). This library uses zero heap.
Copy the JSON folder into your Arduino libraries/ directory, or install via the Arduino Library Manager / PlatformIO registry.
; platformio.ini
lib_deps = akkoyun/JSON#include <JSON.h>
char buf[] = "{\"Event\":900,\"Temp\":23.75,\"Online\":true}";
JSON json(buf); // buf must stay alive as long as json is used
json.isValid(); // true
json.getInt(F("Event")); // 900
json.getFloat(F("Temp")); // 23.75
json.getBool(F("Online")); // 1JSON json(const char * buffer);The caller owns buffer and must keep it alive for the lifetime of the JSON object. No copy is made.
| Method | Returns | Description |
|---|---|---|
isValid() |
bool |
true when the JSON block is structurally valid |
size() |
uint16_t |
Character count from { to } inclusive |
hasKey(F("key")) |
bool |
true when the key exists as a direct child |
typeOf(F("key")) |
Type |
Value type (see enum below) |
Type enum values
| Constant | Value | Meaning |
|---|---|---|
JSON::NONE |
0 | Key not found |
JSON::INT |
1 | Integer number |
JSON::FLOAT |
2 | Decimal number |
JSON::STRING |
3 | Quoted string |
JSON::BOOL |
4 | true / false |
JSON::NUL |
5 | null |
JSON::OBJECT |
6 | Nested { } |
JSON::ARRAY |
7 | Array [ ] |
int32_t getInt (F("key")) // 0 when absent
int32_t operator[](F("key")) // alias for getInt
float getFloat (F("key")) // 0.0 when absent
int8_t getBool (F("key")) // 1=true 0=false -1=absent
bool getString(F("key"), char* buf, uint8_t len) // true on successgetString decodes \\, \", \/, \n, \r, \t escape sequences and always null-terminates buf.
JSON child = json.getObject(F("key"));
JSON child = json.getObject(F("key"), uint8_t index); // object inside arrayThe child JSON shares the parent buffer β zero additional RAM beyond the 7-byte instance. Returns an invalid JSON (isValid() == false) when the key is absent or the value is not an object. Nesting depth is unlimited.
// {"Pressure":{"Settings":{"P_Min":0.5,"P_Max":9.5}}}
JSON pressure = json.getObject(F("Pressure"));
JSON settings = pressure.getObject(F("Settings"));
float pMin = settings.getFloat(F("P_Min")); // 0.5uint8_t arraySize(F("key")) // 0 when absent
int32_t getInt (F("key"), uint8_t index)
float getFloat (F("key"), uint8_t index)
int8_t getBool (F("key"), uint8_t index)
bool getString(F("key"), uint8_t index, char* buf, uint8_t len)
JSON getObject(F("key"), uint8_t index)#include <JSON.h>
char buf[] = "{\"Event\":900,\"DeviceID\":\"ABC-001\",\"Online\":true}";
JSON json(buf);
json.getInt(F("Event")); // 900
json.getBool(F("Online")); // 1
json.hasKey(F("Foo")); // 0
char id[8];
json.getString(F("DeviceID"), id, sizeof(id)); // "ABC-001"char buf[] = "{\"Temp\":23.75,\"RSSI\":-87,\"Label\":\"OK\"}";
JSON json(buf);
json.typeOf(F("Temp")); // JSON::FLOAT (2)
json.typeOf(F("RSSI")); // JSON::INT (1)
json.typeOf(F("Label")); // JSON::STRING (3)
json.getFloat(F("Temp")); // 23.75
json.getInt(F("RSSI")); // -87char buf[] = "{\"Codes\":[10,20,30],\"Tags\":[\"v1\",\"v2\"]}";
JSON json(buf);
json.arraySize(F("Codes")); // 3
json.getInt(F("Codes"), 0); // 10
json.getInt(F("Codes"), 2); // 30
char tag[4];
json.getString(F("Tags"), 1, tag, sizeof(tag)); // "v2"char buf[] =
"{\"ServerIP\":\"176.235.232.121\","
"\"OnInterval\":5,\"VRMS_Min\":192.0,\"VRMS_Max\":253.0,"
"\"Pressure\":{\"Settings\":{\"P_Min\":0.5,\"P_Max\":9.5}}}";
JSON json(buf);
char ip[20];
json.getString(F("ServerIP"), ip, sizeof(ip)); // "176.235.232.121"
json.getFloat(F("VRMS_Min")); // 192.0
JSON settings = json.getObject(F("Pressure")).getObject(F("Settings"));
settings.getFloat(F("P_Min")); // 0.5
settings.getFloat(F("P_Max")); // 9.5// Module output: +CMQTTRXPAYLOAD: 0,42\r\n{"Temp":23.75}
char buf[] = "+CMQTTRXPAYLOAD: 0,42\r\n{\"Temp\":23.75}";
JSON json(buf); // prefix before '{' is ignored automatically
json.getFloat(F("Temp")); // 23.75Why F("key")?
On AVR, string literals live in RAM by default. F() marks them PROGMEM so they stay in Flash. Every key in this library is read with pgm_read_byte(), keeping your RAM budget for actual data.
Why no StaticJsonDocument equivalent?
This library does not pre-allocate a scratch buffer at all. Parsing is done by scanning the source string in-place. Memory cost is O(1) β always 7 bytes per instance, independent of payload size.
Depth of getObject() chains
Each getObject() call returns a 7-byte stack instance. Chains of any depth are safe as long as the parent buffer remains alive.
Copyright (C) 2014-2025 Mehmet Gunce Akkoyun. All rights reserved.
Cannot be copied and/or distributed without the express permission of the author.