Parsing de JSON com jsmn ou cJSON
1. Introdução ao JSON em C e Motivação para Bibliotecas Leves
1.1. Por que não usar uma biblioteca completa?
Em projetos embarcados, sistemas operacionais mínimos ou servidores HTTP com restrições severas de memória, bibliotecas como json-c ou Jansson podem ser excessivas. Elas introduzem dependências pesadas, alocação dinâmica intensiva e um footprint de binário que pode inviabilizar o uso em microcontroladores com poucos kilobytes de RAM. Além disso, muitas vezes o desenvolvedor precisa apenas de parsing básico, sem manipulação complexa da árvore JSON.
1.2. Visão geral dos dois contendores
jsmn é um tokenizer minimalista — não aloca memória, não constrói árvore DOM. Ele apenas identifica tokens (strings, números, objetos, arrays) e retorna offsets no buffer original. Ideal quando o controle de memória é crítico.
cJSON implementa uma árvore DOM completa com alocação dinâmica. Cada nó é um cJSON que armazena tipo, valor e ponteiros para filhos, irmãos e pai. Oferece acesso direto a campos aninhados via funções como cJSON_GetObjectItem().
1.3. Critérios de escolha
- Tamanho do binário: jsmn ocupa ~200 bytes de código compilado; cJSON ~8KB.
- Consumo de memória: jsmn usa apenas tokens (20 bytes cada); cJSON aloca nós na heap.
- Facilidade de acesso: cJSON é muito mais produtivo para JSON aninhado; jsmn exige navegação manual com
strncmp.
2. Instalação e Integração nos Projetos
2.1. jsmn: arquivo único
Basta copiar jsmn.h para o diretório do projeto. Nenhuma compilação separada é necessária:
#include "jsmn.h"
jsmn_parser parser;
jsmntok_t tokens[64]; // buffer estático de tokens
2.2. cJSON: via submodule ou arquivos fonte
Adicione como submodule Git ou copie cJSON.c e cJSON.h:
git submodule add https://github.com/DaveGamble/cJSON.git
No código, inclua e compile junto:
#include "cJSON/cJSON.h"
2.3. Configuração de Makefile/CMake
Makefile mínimo para ambos:
CFLAGS = -Wall -Wextra -std=c99
OBJS = main.o cJSON.o
all: programa
programa: $(OBJS)
$(CC) -o $@ $^
main.o: main.c jsmn.h cJSON.h
cJSON.o: cJSON.c cJSON.h
3. Parsing com jsmn – Tokenização Sem Alocação
3.1. Estruturas fundamentais
typedef struct {
int type; // JSMN_OBJECT, JSMN_ARRAY, JSMN_STRING, JSMN_PRIMITIVE
int start; // offset inicial no buffer
int end; // offset final (exclusivo)
int size; // número de filhos (para objetos/arrays)
} jsmntok_t;
typedef struct {
unsigned int pos; // posição atual no parsing
int toknext; // próximo token disponível
int toksuper; // token pai
} jsmn_parser;
3.2. Fluxo de parsing
jsmn_parser parser;
jsmntok_t tokens[128];
int r;
jsmn_init(&parser);
r = jsmn_parse(&parser, json_string, strlen(json_string), tokens, 128);
if (r < 0) {
// tratar erro: JSMN_ERROR_INVAL, JSMN_ERROR_PART, JSMN_ERROR_NOMEM
}
// tokens[0] é o token raiz
3.3. Exemplo prático
const char *json = "{\"nome\":\"João\",\"idade\":30,\"ativo\":true}";
jsmn_parser parser;
jsmntok_t tokens[64];
jsmn_init(&parser);
int num_tokens = jsmn_parse(&parser, json, strlen(json), tokens, 64);
if (num_tokens < 1) {
printf("Erro no parsing\n");
return;
}
// tokens[0] é o objeto raiz, tokens[1..num_tokens-1] são os filhos
for (int i = 1; i < num_tokens; i++) {
if (tokens[i].type == JSMN_STRING) {
// extrair chave
int len = tokens[i].end - tokens[i].start;
char chave[64];
snprintf(chave, len+1, "%s", json + tokens[i].start);
// o próximo token (i+1) é o valor
i++;
if (tokens[i].type == JSMN_PRIMITIVE) {
int vlen = tokens[i].end - tokens[i].start;
char valor[64];
snprintf(valor, vlen+1, "%s", json + tokens[i].start);
printf("%s: %s\n", chave, valor);
}
}
}
4. Acessando Dados com jsmn – Navegação Manual
4.1. Percorrendo objetos
// Função auxiliar para encontrar valor por chave
int find_key(jsmntok_t *tokens, int num_tokens, const char *json, const char *key) {
for (int i = 1; i < num_tokens; i++) {
if (tokens[i].type == JSMN_STRING) {
int len = tokens[i].end - tokens[i].start;
if (len == strlen(key) && strncmp(json + tokens[i].start, key, len) == 0) {
return i + 1; // token do valor
}
}
}
return -1;
}
int idx = find_key(tokens, num_tokens, json, "idade");
if (idx != -1 && tokens[idx].type == JSMN_PRIMITIVE) {
int idade = atoi(json + tokens[idx].start);
printf("Idade: %d\n", idade);
}
4.2. Arrays aninhados
// JSON: {"pessoas":[{"nome":"Ana"},{"nome":"Beto"}]}
// tokens[0]: objeto raiz (size=1)
// tokens[1]: chave "pessoas"
// tokens[2]: array (size=2)
// tokens[3]: objeto 0 (size=1)
// tokens[4]: chave "nome"
// tokens[5]: string "Ana"
// tokens[6]: objeto 1 (size=1)
// tokens[7]: chave "nome"
// tokens[8]: string "Beto"
int array_idx = find_key(tokens, num_tokens, json, "pessoas");
if (array_idx != -1 && tokens[array_idx].type == JSMN_ARRAY) {
int num_pessoas = tokens[array_idx].size;
int current = array_idx + 1;
for (int p = 0; p < num_pessoas; p++) {
// cada objeto começa em current
int obj_size = tokens[current].size;
// pular para o próximo objeto
current += obj_size + 1; // +1 para o próprio objeto
}
}
4.3. Limitações
- Conversão manual: números precisam de
atoi(),atof(),strtol(). - Strings escapadas: jsmn não desescapa
\",\n,\\— você precisa implementar. - Sem validação semântica: jsmn apenas verifica sintaxe, não se os valores são esperados.
5. Parsing com cJSON – Árvore DOM e Manipulação Direta
5.1. Estruturas
typedef struct cJSON {
struct cJSON *next, *prev; // para listas ligadas (objetos/arrays)
struct cJSON *child; // primeiro filho
int type; // cJSON_Invalid, cJSON_False, cJSON_True, cJSON_NULL, cJSON_Number, cJSON_String, cJSON_Array, cJSON_Object
char *valuestring; // se for string
int valueint; // se for número inteiro
double valuedouble; // se for número real
char *string; // nome da chave (em objetos)
} cJSON;
5.2. Parsing completo
const char *json = "{\"usuario\":{\"nome\":\"Maria\",\"email\":\"maria@exemplo.com\",\"tags\":[\"admin\",\"dev\"]}}";
cJSON *root = cJSON_Parse(json);
if (root == NULL) {
const char *error = cJSON_GetErrorPtr();
printf("Erro: %s\n", error);
return;
}
cJSON *usuario = cJSON_GetObjectItem(root, "usuario");
if (usuario != NULL) {
cJSON *nome = cJSON_GetObjectItem(usuario, "nome");
if (cJSON_IsString(nome)) {
printf("Nome: %s\n", nome->valuestring);
}
cJSON *tags = cJSON_GetObjectItem(usuario, "tags");
if (cJSON_IsArray(tags)) {
int tamanho = cJSON_GetArraySize(tags);
for (int i = 0; i < tamanho; i++) {
cJSON *tag = cJSON_GetArrayItem(tags, i);
printf("Tag %d: %s\n", i, tag->valuestring);
}
}
}
cJSON_Delete(root); // sempre liberar!
5.3. Exemplo prático com JSON complexo
const char *json = "{ \"produtos\": [ { \"id\": 1, \"preco\": 29.90, \"disponivel\": true }, { \"id\": 2, \"preco\": 0 } ] }";
cJSON *root = cJSON_Parse(json);
cJSON *produtos = cJSON_GetObjectItem(root, "produtos");
int count = cJSON_GetArraySize(produtos);
for (int i = 0; i < count; i++) {
cJSON *prod = cJSON_GetArrayItem(produtos, i);
int id = cJSON_GetObjectItem(prod, "id")->valueint;
double preco = cJSON_GetObjectItem(prod, "preco")->valuedouble;
cJSON *disp = cJSON_GetObjectItem(prod, "disponivel");
bool disponivel = cJSON_IsTrue(disp);
printf("Produto %d: R$%.2f %s\n", id, preco, disponivel ? "(disponível)" : "");
}
cJSON_Delete(root);
6. Comparação de Desempenho e Consumo de Recursos
6.1. Benchmark conceitual
| JSON | jsmn (tempo) | jsmn (memória) | cJSON (tempo) | cJSON (memória) |
|---|---|---|---|---|
| 1KB | ~5 µs | 20 bytes/token | ~15 µs | ~2KB heap |
| 10KB | ~50 µs | 20 bytes/token | ~120 µs | ~15KB heap |
| 100KB | ~500 µs | 20 bytes/token | ~1.2 ms | ~150KB heap |
6.2. jsmn: overhead mínimo
- Cada token ocupa 20 bytes (3 ints + padding).
- Para um JSON de 100KB com ~2000 tokens, memória total: 40KB.
- Navegação manual adiciona complexidade O(n²) no pior caso.
6.3. cJSON: facilidade versus fragmentação
- Cada nó
cJSONocupa ~64-80 bytes (ponteiros, strings, valores). - Para o mesmo JSON de 100KB, ~2000 nós = 120-160KB.
- Alocações frequentes podem causar fragmentação em sistemas com pouca RAM.
7. Tratamento de Erros e Edge Cases
7.1. jsmn: códigos de erro
int r = jsmn_parse(&parser, json, len, tokens, 128);
switch (r) {
case JSMN_ERROR_INVAL: // JSON malformado (chaves desbalanceadas, etc.)
case JSMN_ERROR_PART: // JSON incompleto (faltam dados)
case JSMN_ERROR_NOMEM: // buffer de tokens insuficiente
}
7.2. cJSON: verificação de ponteiro nulo
cJSON *item = cJSON_Parse(json);
if (item == NULL) {
// erro de parsing
}
cJSON *campo = cJSON_GetObjectItem(item, "inexistente");
if (campo == NULL) {
// campo não encontrado
}
// Nunca assuma que o tipo é o esperado
if (!cJSON_IsNumber(campo)) {
// tratar tipo inesperado
}
7.3. Casos especiais
// Strings com aspas escapadas: jsmn retorna o raw; cJSON desescapa automaticamente
// Números em notação científica: cJSON treat como double; jsmn retorna raw
// Valores nulos: cJSON_IsNull() no cJSON; jsmn retorna JSMN_PRIMITIVE com "null"
8. Conclusão e Boas Práticas para Projetos em C
8.1. Quando usar jsmn
- Firmware para microcontroladores (STM32, ESP32) com heap mínimo.
- Kernels ou sistemas operacionais que não suportam
malloc. - Parsing de JSON extremamente simples (poucos campos, sem aninhamento).
- Quando o binário final precisa ser < 1KB para parsing.
8.2. Quando usar cJSON
- Servidores HTTP, ferramentas CLI, aplicações desktop.
- Prototipagem rápida com JSON aninhado e acesso direto a campos.
- Quando a produtividade do desenvolvedor é mais importante que cada byte.
- Projetos que já usam alocação dinâmica extensivamente.
8.3. Dicas finais
- jsmn não requer
free()— os tokens são alocados estaticamente ou na stack. - cJSON sempre requer
cJSON_Delete()para evitar vazamento de memória. - Evite parsing duplicado do mesmo JSON: parseie uma vez e reutilize a árvore/tokens.
- Considere uma abordagem híbrida: use jsmn para validação inicial e cJSON para acesso posterior em sistemas com memória suficiente.
Referências
- Documentação oficial do jsmn — Repositório com código-fonte, exemplos e especificação da API tokenizer.
- Documentação oficial do cJSON — Repositório principal com documentação completa da API DOM.
- Tutorial: Parsing JSON in C with jsmn — Artigo técnico da Embedded.com com exemplos práticos para sistemas embarcados.
- cJSON Tutorial – A Lightweight JSON Parser for C — Tutorial focado em ESP32/Arduino, mas aplicável a qualquer projeto C.
- Comparação de bibliotecas JSON para C — Benchmark e análise comparativa entre jsmn, cJSON, json-c e Jansson.
- Manual de referência JSON (RFC 8259) — Especificação oficial do formato JSON, útil para entender edge cases.
- Boas práticas para parsing de JSON em C embarcado — Artigo da Barr Group sobre técnicas de parsing com restrições de memória.