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ó cJSON ocupa ~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