Again playing with EnTT (ECS) which has a builtin mechanism that helps you (de)serialize a registry. You have to implement an 'archive' that (de)serializes the data.

Here in the source-code I use nlohmann::json for this. In the end it was simpler than it seemed at the beginning.

The Input/Output-Archives:

#pragma once

#include <nlohmann/json.hpp>
#include <entt/entt.hpp>

// nlohmann-json based entt-(de)serialization
class NJSONOutputArchive {
        root = nlohmann::json::array();

    // new element for serialization. giving you the amount of elements that is going to be
    // pumped into operator()(entt::entity ent) or operator()(entt::entity ent, const T &t)
    void operator()(std::underlying_type_t<entt::entity> size){
        int a=0; 
        if (!current.empty()){
        current = nlohmann::json::array();
        current.push_back(size); // first element of each array keeps the amount of elements. 

    // persist entity ids
    void operator()(entt::entity entity){
        // Here it is assumed that no custom entt-type is chosen

    // persist components
    // ent is the entity and t a component that is attached to it
    // in json we first push the entity-id and then convert the component
    // to json just by assigning:  'nlohmann:json json=t'
    // For this to work all used component musst have following in its body:
    // NLOHMANN_DEFINE_TYPE_INTRUSIVE([component_name], fields,....)
    // e.g.
    // struct Transform {
    //     float x;
    //     float y;
    //     NLOHMANN_DEFINE_TYPE_INTRUSIVE(Transform, x,y)
    // };
    template<typename T>
    void operator()(entt::entity ent, const T &t){
        current.push_back((uint32_t)ent); // persist the entity id of the following component
        // auto factory = entt::type_id<T>();
        // std::string component_name = std::string(; 
        // current.push_back(component_name);

        nlohmann::json json = t;

    void Close(){
        if (!current.empty()){

    // create a json as string
    const std::string AsString() {
        std::string output = root.dump();
        return output;

    // create bson-data
    const std::vector<uint8_t> AsBson(){
        std::vector<std::uint8_t> as_bson = nlohmann::json::to_bson(root);
        return as_bson;

    nlohmann::json root;
    nlohmann::json current;

class NJSONInputArchive {
    nlohmann::json root;
    nlohmann::json current;

    int root_idx=-1;
    int current_idx=0;

    NJSONInputArchive(const std::string& json_string)
        root = nlohmann::json::parse(json_string);


    void next_root(){
        if (root_idx >= root.size()){
            // ERROR
        current = root[root_idx];
        current_idx = 0;

    void operator()(std::underlying_type_t<entt::entity> &s){
        int size = current[0].get<int>();
        s = (std::underlying_type_t<entt::entity>)size; // pass amount to entt

    void operator()(entt::entity &entity){
        uint32_t ent = current[current_idx].get<uint32_t>();
        entity = entt::entity(ent);

    template<typename T>
    void operator()(entt::entity &ent, T &t){
        nlohmann::json component_data = current[current_idx*2];

        auto comp = component_data.get<T>();
        t = comp;

        uint32_t _ent = current[current_idx*2-1];
        ent = entt::entity(_ent); // last element is the entity-id

Some sample structs:

#include <nlohmann/json.hpp>
#include <entt/entt.hpp>
#include <string>

struct Tower{
    std::string name;
    int type_id;
    float range;

    NLOHMANN_DEFINE_TYPE_INTRUSIVE(Tower, name, type_id, range)

struct Walker {
    int type_id;
    float speed;
    entt::entity target;

    NLOHMANN_DEFINE_TYPE_INTRUSIVE(Walker, type_id,speed,target)

struct Transform {
    float x;
    float y;


And test it:

void test()
    entt::registry reg;
    auto e1 = reg.create();
    Tower t{"Tower", 1895, 18.95f}; = "hansi";
    Tower tw1 = reg.emplace<Tower>(e1, std::string("tower1"), 1895, 18.95f);
    reg.emplace<Transform>(e1, 1.0f, 1.0f);

    auto e2 = reg.create();
    Tower tw2 = reg.emplace<Tower>(e2, "tower2", 18, 0.95f);
    reg.emplace<Transform>(e2, 5.0f, 5.0f);

    auto w1 = reg.create();
    Walker &w = reg.emplace<Walker>(w1, 1, 0.5f);
    reg.emplace<Transform>(w1, 100.0f, 100.0f);

    NJSONOutputArchive json_archive;
    entt::basic_snapshot snapshot(reg);
        .component<Tower, Walker, Transform>(json_archive);
    std::string json_output = json_archive.AsString();
    printf("json:%s", json_output.c_str());

    NJSONInputArchive json_in(json_output);
    entt::registry reg2;
    entt::basic_snapshot_loader loader(reg2);
        .component<Tower, Walker, Transform>(json_in);

    auto _e1 = entt::entity(0);
    auto _e2 = entt::entity(1);
    auto [tower, transform] = reg2.get<Tower, Transform>(e1);
    auto [tower2, transform2] = reg2.get<Tower, Transform>(e2);

This will result in following JSON:


Line 1(EntityIDs): [0]=amount of entities,[1-3]=entity-ids,[4]=destroyed(not sure what this is and why it is serialized)
Line 2(Tower): [0]=amount of components,[1,3]=entity-id,[2,4]=component data to be attached to the entity of [1,3]
Line 3(Walker):[0]=amount of components,[1]=entity-id.....
Line 4(Transform):....

Let's recall that how we called the snapshot:

        .component<Tower, Walker, Transform>(json_archive);

You see that the component serialization was in exactly the order that we specified and it is important that the order is in the same in the snapshot_loader....