Examples of how to use Sonic-Cpp.
- Copy all files under
include/into your project include path - or using compiler option, such as
-I/path/to/sonic/include.
Sonic-Cpp is a header-only library, you only need to add -mavx2 -mpclmul -mbmi
or -march=haswell to support.
Sonic-Cpp assumes all input strings are encoded using UTF-8 and won't verify by default
The simplest way is calling Parse() methods:
#include "sonic/sonic.h"
// ...
std::string json = "[1,2,3]";
sonic_json::Document doc;
doc.Parse(json);
if (doc.HasParseError()) {
// error path
// If parse failed, the type of doc is null.
}#include "sonic/sonic.h"
// ...
sonic_json::WriteBuffer wb;
doc.Serialize(wb);
std::cout << wb.ToString() << std::endl;Node is the present for JSON value and supports all JSON value manipulation.
Document is the manager of Nodes. Sonic-Cpp organizes JSON value as a tree. Document also the root of JSON value tree. There is an allocator in Document, which you should use to allocate memory for Node and Document.
Note: Re-parsing a
Documentdiscards the previous tree. Any raw pointers, iterators, orDNode*obtained from an earlierParse()become invalid and must be re-acquired after each parse.
There are two ways to find members: operator[] or FindMember. We recommend
using FindMember.
#include "sonic/sonic.h"
// ...
using AllocatorType = typename sonic_json::Allocator;
sonic_json::Node node;
AllocatorType alloc;
// Add members for node
// find member by key
if (node.IsObject()) { // Note: CHECK NODE TYPE IS VERY IMPORTANT.
const char* key = "key";
auto m = node.FindMember(key); // recommended
if (m != node.MemberEnd()) {
// do something
}
}
// Second method
if (node.IsObject()) {
const char* key1 = "key1";
const char* key2 = "key2";
// You must ensure that all keys exist.
sonic_json::Node& val = node[key1][key2];
// If key doesn't exist, operator[] will return reference to a static node
// which type is Null. You SHOULD NOT MODIFY this static node. In this case,
// FindMember is a better choice.
if (val.IsNull()) {
// error path
}
}sonic_json::Node node(0.0);
if (node.IsDouble()) {
std::cout << node.GetDouble() << std::endl;
}
node.SetInt(0);
if (node.IsInt64()) {
std::cout << node.GetInt64() << std::endl;
}The following Is*, Get* and Set* methods are supported:
- IsNull(), SetNull()
- IsBoo(), GetBool(), SetBool(bool)
- IsString(), GetString(), GetStringView(), SetString(const char*, size_t)
- IsNumber()
- IsArray(), SetArray()
- IsObject(), SetObject()
- IsTrue(), IsFalse()
- IsDouble(), GetDouble(), SetDouble(double)
- IsInt64(), GetInt64(), SetInt64(int64_t)
- IsUint64(), GetUint64(), SetUint64_t(uint64_t)
Note: GetString() will return std::string. GetStringView() has better performance.
AddMember method only accepts rvalue as the argument.
using NodeType = sonic_json::Node;
sonic_json::Document doc;
auto& alloc = doc.GetAllocator();
doc.SetObject();
doc.AddMember("key1", NodeType(1), alloc);
{
NodeType node;
node.SetArray();
doc.AddMember("key2", std::move(node), alloc);
}
sonic_json::WriteBuffer wb;
doc.Serialize(wb);
std::cout << wb.ToString() << std::endl; // {"key1": 1, "key2":[]}using NodeType = sonic_json::Node;
sonic_json::Document doc;
auto& alloc = doc.GetAllocator();
doc.SetArray();
doc.PushBack(NodeType(1.0), alloc);
{
NodeType node;
node.SetObject();
doc.PushBack(node, alloc);
}
sonic_json::WriteBuffer wb;
doc.Serialize(wb);
std::cout << wb.ToString() << std::endl; // [1.0, {}]// doc = {"a": 1, "b": 2, "c": 3}
if (doc.IsObject()) {
const char* key = "a";
if (doc.RemoveMember(key)) {
std::cout << "Remove " << key << " successfully!\n";
} else {
std::cout << "Object doesn't have " << key << "!\n";
}
}There are 2 methods to remove elements in array: PopBack() pops the last
element and Erase() removes range elements.
// doc = [1, 2, 3, 0]
doc.PopBack(); // [1, 2, 3]
doc.Erase(doc.Begin(), doc.Begin() + 1); // [start, end), [2, 3]Sonic provides GetParseError() to get the parse error code and GetErrorOffset
to get the position of the last parsing error.
Example:
#include <iostream>
#include <string>
#include "sonic/sonic.h"
void parse_json(const std::string& data) {
sonic_json::Document doc;
doc.Parse(data);
if (doc.HasParseError()) {
std::cout << sonic_json::ErrorMsg(doc.GetParseError()) << std::endl
<< "Json: \n" << data << std::endl
<< "Error offset is: " << doc.GetErrorOffset() << std::endl;
} else {
std::cout << "Parse json:\n" << data << "\n successfully";
}
}
Sonic node has a template parameter Allocator. Users can define their
Allocator. If you want to provide a new allocator, you can define node and
document as:
using MyNode = sonic_json::DNode<MyAllocator>;
using MyDoc = sonic_json::GenericDocument<MyNode>;Sonic uses rapidjson's allocator, you can define your own allocator follow rapidjson allocaotr
DNode mutations like PushBack, AddMember, and Reserve do not return a
status code. When you use MemoryPoolAllocator, you can check
HadOom() / ClearOom() around these operations if you need to detect an
allocation failure:
auto& alloc = doc.GetAllocator();
alloc.ClearOom();
doc.PushBack(v, alloc);
if (alloc.HadOom()) { /* handle OOM */ }The flag is sticky until cleared. This is a MemoryPoolAllocator feature, not
part of the abstract allocator concept.
Sonic provides a JsonPointer class but doesn't support resolving the JSON pointer syntax of RFC 6901. We will support it in the future.
#include "sonic/sonic.h"
// Sonic JSON pointer need a template parameter that describes using which string
// type, such as std::string and std::string_view. string_view can avoid coping
// string data. But, the user should keep the memory is always valid when using
// string_view as template parameter.
using PointerType = sonic_json::GenericJsonPointer<sonic_json::StringView>;
// Sonic also defines a typename JsonPointerView using string_view.
// example:
// using PointerType = sonic_json::JsonPointerView;
// The typename JsonPointer in sonic is defined as:
// using JsonPointer =
// GenericJsonPointer<SONIC_JSON_POINTER_NODE_STRING_DEFAULT_TYPE>;
// The macro SONIC_JSON_POINTER_NODE_STRING_DEFAULT_TYPE is std::string.
// query by JSON pointer
sonic_json::Document doc;
/* parsing code */
/* ... */
// Construct JSON pointer by initializer list.
sonic_json::Node* node1 = doc.AtPointer(PointerType({"a"}));
// error check!!!
if (node1 != nullptr) {
std::cout << "/a exists!\n";
} else {
std::cout << "/a doesn't exist!\n";
}
sonic_json::Node* node2 = doc.AtPointer(PointerType({"b", 1, "a"}));
if (node2 != nullptr) {
std::cout << "/b/1/a Eixsts!\n";
} else {
std::cout << "/b/1/a doesn't exist!\n";
}
There is an optimized implementation of AtPointer when the argument is string literal.
sonic_force_inline const NodeType* AtPointer() const { return downCast(); }
template <typename... Args>
sonic_force_inline const NodeType* AtPointer(size_t idx, Args... args) const {
if (!IsArray()) {
return nullptr;
}
if (idx >= Size()) {
return nullptr;
}
return (*this)[idx].AtPointer(args...);
}
template <typename... Args>
sonic_force_inline const NodeType* AtPointer(StringView key,
Args... args) const {
if (!IsObject()) {
return nullptr;
}
auto m = FindMember(key);
if (m == MemberEnd()) {
return nullptr;
}
return m->value.AtPointer(args...);
}Actually, std::string_view sv; sv == "hello world" is faster than
std::string s; s == "hello world". The compiler will optimize for string_view.
The above implementation can avoid converting string literal to std::string.
Example:
sonic_json::Node* node3 = doc.AtPointer("b", 1, "b");
if (node3 != nullptr) {
std::cout << "/b/1/b Eixsts!\n";
} else {
std::cout << "/b/1/b doesn't exist!\n";
}Sonic supports parsing specific Json Value by JSON pointer. The target JSON Value can be anyone (object, array, string, number...).
#include "sonic/sonic.h"
std::string json = R"(
{
"a": {
"a0":[0,1,2,3,4,5,6,7,8,9],
"a1": "hi"
},
"b":[
{"b0":1},
{"b1":2}
]
}
)";
int main() {
// The target exists in JSON
{
sonic_json::Document doc;
// doc only contain one Json Value: /a/a0/8
doc.ParseOnDemand(json, {"a", "a0", 8});
if (doc.HasParseError()) {
return -1;
}
uint64_t val = doc.GetUint64();
std::cout << "Parse OnDemand result is " << val << std::endl;
// output: Parse OnDemand result is 8
}
// The target does not exist in JSON
{
sonic_json::Document doc;
doc.ParseOnDemand(json, {"a", "a1", "unknown"});
if (doc.HasParseError()) {
sonic_json::SonicError err = doc.GetParseError();
size_t error_position = doc.GetErrorOffset();
std::cout << "Parse Error: " << sonic_json::ErrorMsg(err)
<< ". Error Position At " << error_position << std::endl;
// output: Parse Error: ParseOnDemand: the target type is not matched..
// Error Position At 55
}
}
return 0;
}The members of JSON object value are organized as a vector in Sonic-cpp. This
makes Sonic-cpp parsing fast but maybe causes the query slow when the object
size is very large. Sonic-cpp provides CreateMap method to create a
std::multimap. This map records every member index in vector. The FindMember
method will use the map first if it exists. Actually, using a map isn't always
fast, especially when the object size is small. The users can call the
DestroyMap method to destroy the created map.
Example:
#include "sonic/sonic.h"
#include <iostream>
#include <string>
std::string get_json_string() {
return R"(
{
"a":[
{"b":1, "c":2, "d":3, "e":4}
]
}
)";
}
int main() {
std::string json = get_json_string();
sonic_json::Document doc;
if (doc.Parse(json).HasParseError()) {
std::cout << "Parse failed!\n";
return -1;
}
sonic_json::Node* node = doc.AtPointer("a", 0);
if (node == nullptr || !node->IsObject()) {
std::cout << "/a/0 doesn't exist or isn't an object!\n";
return -1;
}
if (node->FindMember("e") == node->MemberEnd()) {
std::cout << "/a/0/e doesn't exist!\n";
}
// Create a map. If node already has a map, do nothing.
node->CreateMap(doc.GetAllocator()); // Need Allocator
// Use the map to query. This is same as above.
if (node->FindMember("e") == node->MemberEnd()) {
std::cout << "/a/0/e doesn't exist!\n";
}
// Not need the map anymore.
node->DestroyMap();
std::cout << "Querying finish!\n";
return 0;
}