Parsing JSON with C - libjson-c
Motivação
Não havia algo concretizado durante a escrita deste artigo. A princípio apenas comecei a desenvolver uma ferramenta em C que utilizava JSON como arquivo de configuração. Foi divertido aprender a criar o parser, porém, mesmo decidido que vou usar outra linguagem para isso, decidi postar.
Objetivo
Fazer com que a ferramenta seja dinâmica ao ponto de usar um arquivo JSON para manipular os dados, em principal, adicionar e extrair valores através das chaves. Este artigo é produzido conforme avanço no “projeto” (por ora, projeto bem entre aspas mesmo).
Instalando dependência
Antes de qualquer coisa, certifique-se que o pacote “libjson-c-dev” (deb) ou “json-c-devel” (rpm) esteja instalado. Em alternativa, a instalação direto do código fonte é simples. Veja:
$ git clone https://github.com/json-c/json-c
$ mkdir json-c/json-c-build
$ cd !$
$ cmake ../../json-c
Observação: o truque “cd !$” vai depender do terminal em uso; por exemplo, não funciona no Fish Shell e no SH, mas, funciona no Bash.
Antes de fazer a instalação, recomendo que realize alguns testes (com valgrind):
$ make test
# somente execute se tiver valgrind instalado, claro.
$ make USE_VALGRIND=0 test
Se deu tudo certo até aqui, prossiga a instalação:
$ sudo make install
Construindo o JSON Parsing
Com a libjson-c instalada, vou seguir com a criação de um JSON parsing simples para facilitar a implementação dos próximos recursos.
#include <stdio.h>
#include <json-c/json.h>
int main(void) {
struct json_object *pj; /* parsed json */
struct json_object *bk; /* band key */
struct json_object *sk; /* song key */
pj = json_tokener_parse("{\"band\":\"Slayer\",\"song\":\"Piano Wire\"}");
json_object_object_get_ex(pj, "band", &bk);
json_object_object_get_ex(pj, "song", &sk);
printf("%s - %s", json_object_get_string(bk), json_object_get_string(sk));
return EXIT_SUCCESS;
}
Basta compilar, e não esquecer de mencionar a flag da biblioteca:
$ gcc -Wall main.c -o main -ljson-c
$ ./main
Slayer - Piano Wire
Maneiro! Apesar da simplicidade.
A função json_tokener_parse()
retorna um objeto JSON válido, o que a torna bastante útil quando já existe os dados em JSON, pois também é possível criar do zero através da libjson-c usando a função json_object_new_object()
, pelo nome já fica claro que ela cria um novo objeto. Neste caso, é possível acessar todo o JSON fazendo a conversão para string usando json_object_to_json_string(struct json *obj)
. Exemplo:
parsed_json = json_tokener(JSON_DATA);
printf("%s", json_object_to_json_string(parsed_json));
Considerando que o parsed_json
é um tipo “struct json *obj
”, nele fica fácil de trabalhar para manipular da maneira que quiser, inclusive adicionar outro objeto com
json_object_object_add(
struct json_object *obj,
const char *key,
struct json_object *val)
Por exemplo:
struct json_object *mj = json_object_new_string("Michael Joseph Jackson");
parsed_json = json_tokener_parse(value);
json_object_object_add(parsed_json, "Pop", mj);
printf("%s\n", json_object_to_json_string(parsed_json));
Próxima implementação é usar esse parsing de uma forma mais flexível; por exemplo, imagine utilizar um arquivo JSON como configuração seja lá qual a forma desejada. Para isso, é necessário entender o conceito de estrutura de dados em C porque agora os dados serão acessados através de um objeto numa estrutura.
//--- config.h ---
#ifndef __CONFIG_H__
#define __CONFIG_H__
typedef struct __attribute__((packed)) {
const char *band;
const char *song;
} config_t;
void config_init(config_t *key, char *value);
#endif
//--- /config.h ---
A função json_object_get_string
retorna um const char *
, em vista disso, os membros da estrutura deve ser const char *
. Caso contrário, na ausência do const
, será necessário atribuir um casting char *
em json_object_get_string
. Inclusive, converter um ponteiro que aponta para qualquer tipo de dado para um ponteiro que aponta para um tipo de dado constante é naturalmente aceitável, mas o modo vice-versa não. Por isso que não é recomendado fazer algo do tipo:
(char *)json_object_get_string(key);
Vamos ao código:
// --- config.c ---
#include <stdio.h>
#include <config.h>
#include <json-c/json.h>
void config_init(config *key, char *value) {
struct json_object *pj; /* parsed json */
struct json_object *bk; /* band key */
struct json_object *sk; /* song key */
if (!value) {
fputs("value not setted", stderr);
exit(EXIT_FAILURE);
}
pj = json_tokener_parse(value);
json_object_object_get_ex(pj, "band", &bk);
json_object_object_get_ex(pj, "song", &sk);
key->band = json_object_get_string(bk);
key->song = json_object_get_string(sk);
}
// --- /config.c ---
//--- main.c ---
#include <stdio.h>
#include <json-c/json.h>
#include <config.h>
int main(void) {
config_t conf;
char *json = "{\"band\":\"Slayer\",\"song\":\"Piano Wire\"}";
config_init(&conf, json);
printf("%s - %s\n", conf.band, conf.song);
return EXIT_SUCCESS;
}
//--- /main.c ---
Com as mudanças realizadas, compile o código acima semelhante ao anterior, apenas adicione o -I.
- note o “.” (ponto) logo depois de “I”, especificando para que o diretório atual seja considerado para a inclusão dos cabeçalhos usados os sinais menor-que (<) e maior-que (>) ao invés de aspas duplas.
$ gcc -Wall -I. main.c -o main -ljson-c
$ ./main
Slayer - Piano Wire
Considerações Finais
Como dito no início, este artigo foi escrito enquanto desenvolvia uma ferramenta que utilizava um arquivo JSON como configuração. Apesar de ter adquirido algumas experiências com a biblioteca libjson-c, horas depois consideirei utilizar outro método de configuração após entender que usar JSON como configuração é um improviso que acabou sendo romantizado e não comporta configurações complexas. Isto se dá ao fato de inúmeras limitações inerentes, como a chave com aspa dupla não faz sentido já que é uma CHAVE, não VALOR; o uso de comentário no JSON é outro paliativo, não é todo lugar que pode ser usado “//” nas chaves.
{
"//": "Thrash Metal",
"band": "Slayer",
"//": "Last Album",
"album": "Repentless"
}
Convenhamos também que isso não é algo legível e agradável para se trabalhar; o JSON também não suporta string em multi-linhas, por isso que é necessário o escape da Newline (“\n”) ou CRLF (“\r\n” - Carriage Return & Line Feed) dentre diversos outros problemas que pode ser enfrentado quando usado como arquivo de configuração.
Contudo, quis elucidar o quão fácil é manipular JSON em C começando com algo tão simples conforme demonstrado apenas com o “main.c” e evoluindo com a estrutura etc. Existem diversos projetos que suportam JSON como API ou como configuração, cabe você criar algo interessante agora. :-)