(de)serializing c++ object almost automagically Here's one way to solve the serialization problem in c++20, using a json library:
#include &lt;functional&gt;

#include &lt;unordered_map&gt;

#include &quot;json.hpp&quot;

namespace nlm = nlohmann;

class properties
{
  struct property_info
  {
    std::function&lt;void(nlm::json const&amp;)&gt; deserializor;
    std::function&lt;nlm::json()&gt; serializor;
  };

  std::unordered_map&lt;std::string_view, property_info&gt; reg_;

public:    
  //
  nlm::json state() const;
  void state(nlm::json const&amp;);

  //
  template &lt;typename U, typename ...A&gt;
  auto register_property(std::string_view const&amp; k, U&amp; v, A&amp;&amp; ...a)
  {
    static_assert(!(sizeof...(a) % 2));
    static_assert(!(std::is_const_v&lt;U&gt;));

    reg_.try_emplace(k,
      [&amp;v](nlm::json const&amp; j){v = j.get&lt;U&gt;();},
      [&amp;v]{return nlm::json(v);}
    );

    if constexpr (sizeof...(a))
    {
      register_property(std::forward&lt;A&gt;(a)...);
    }

    return [this](auto&amp;&amp; ...a)
      {
        return register_property(std::forward&lt;decltype(a)&gt;(a)...);
      };
  }

  auto get(std::string_view const&amp; k)
  {
    return reg_.find(k)-&gt;second.serializor();
  }

  template &lt;typename U&gt;
  void set(std::string_view const&amp; k, U&amp;&amp; v)
  {
    reg_.find(k)-&gt;second.deserializor(std::forward&lt;U&gt;(v));
  }
};

nlm::json properties::state() const
{
  nlm::json r(nlm::json::object());

  for (auto i(reg_.cbegin()), cend(reg_.cend()); cend != i; i = std::next(i))
  {
    r.emplace(i-&gt;first, i-&gt;second.serializor());
  }

  return r;
}

void properties::state(nlm::json const&amp; e)
{
  assert(e.is_object());
  auto const cend(reg_.cend());

  for (auto i(e.cbegin()), ecend(e.cend()); ecend != i; i = std::next(i))
  {
    auto&amp; key(i.key());

    if (auto const j(std::as_const(reg_).find(key)); cend != j)
    {
      j-&gt;second.deserializor(i.value());
    }
  }
}

Example:
int main()
{
  struct S: properties
  {
    bool b{};
    int i{};

    S()
    {
      register_property(&quot;b&quot;, b)(&quot;i&quot;, i);
    }
  } s;

  s.set(&quot;b&quot;, true);
  s.set(&quot;i&quot;, 11.1);

  std::cout &lt;&lt; s.get(&quot;b&quot;) &lt;&lt; std::endl;
  std::cout &lt;&lt; s.state() &lt;&lt; std::endl;
}

2 functors for (de)serializing are generated for each registered property. If state is requested or set, these are executed accordingly. Obvious improvements are certain checks, getters/setters, instead of references. I think this is a nice quick solution for simple cases.
https://wandbox.org/permlink/J0vdLOTp5x6xvF9s
Avoid creating namespace aliases in header files
I assume that at least the declaration of class properties would be put in a header file. In that case, consider that users of that header file might not expect namespace nlm to be declared, so I recommend just writing out nlohmann fully.
Spelling
A minor issue: it is serializer, not serializor.
Function names
Avoid overloading state() to mean either setting the state or getting the state. While related, these are different operations, and it is much better to make that explicit by giving them different function names. An obvious modification is to name them get_state() and set_state(), but that sounds quite generic. I would also make it explicit that you are converting to or from JSON, so consider naming them to_json() and from_json().
Overhead
Your serialization method introduces a huge overhead. Every instance of a serializable struct now has to contain a std::unordered_map, which is filled in in the constructor. So this costs time and memory. It would be much nicer if you could build this only once per type that derives from properties. Perhaps it can be done using static variables and CRTP, something like:
template&lt;typename T&gt;
struct properties
{
    struct registry
    {
        // keeps the actual mapping
        ...
    };
    
    template &lt;typename U&gt;
    void set(str::string_view const&amp; k, U&amp;&amp; v) {
        // forward it to the registry object, along with a pointer to the object
        auto self = static_cast&lt;T *&gt;(this);
        self-&gt;registry.set(self, k, v);
    }

    ...
};

struct S: properties&lt;S&gt;
{
    bool b{};
    int i{};
    
    static properties::registry reg_;

public:
    ...
};

S::properties::registry S::reg_ = {{&quot;b&quot;, &amp;S::b}, {&quot;i&quot;, &amp;S::i}};

But I struggle myself with how to create a constructor for properties::registry that would allow the above code (especially the last line) to work.
Make get() const
You should make the get() member function const, as it should not modify the state, and this will allow that functions to be used on const instances of classes that inherit from properties.
Use range-for where possible
You can simplify the code in some places by using range-for. For example, in properties::state(), where you can also combine it with structured binding:
for (auto &amp;[name, variable]: reg_)
{
    r.emplace(name, variable.serializer());
}

It is unfortunate that the iterator of nlm::json doesn't work the same way; you can only access the value in a range-for, not the key.
Crash at runtime when accessing a non-existing property
If in main(), you call s.get(&quot;x&quot;), the program crashes with a segmentation fault. Even if you never expect this function to be called with a user-supplied name, it still makes it hard to debug programming errors. Check the return value of calls to find() before trying to dereference the result. You could throw a std::runtime_error if find() return nulltpr, or if you don't want to use exceptions or pay for the performance cost in production builds, at least use assert() to help with debug builds.