Migrate to libyang2

libnetconf2: getSchema and getConfig were no longer used in netconf-cli,
so I deleted them. They can get readded once the bindings get split into
a separate project.

sysrepo_access: Some sr_val stuff was removed.

YangSchema: type descriptions are not available
            availableNodes returns only input nodes for RPC nodes
            impl_getSchemaNode: no longer disables error printing

libyang: No longer supports leafrefs without the leaf it points to.

Depends-on: https://cesnet-gerrit-czechlight/c/CzechLight/dependencies/+/5171
Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/5171
Change-Id: Ie49381a003a61a7bb028be7b2fa1d9d926ac4e58
diff --git a/.zuul.public.yaml b/.zuul.public.yaml
index 7582f50..117b3ac 100644
--- a/.zuul.public.yaml
+++ b/.zuul.public.yaml
@@ -8,16 +8,22 @@
     check:
       jobs:
         - f34-gcc-cover:
+            pre-run: ci/pre.yaml
             requires: CzechLight-deps-f34-gcc
         - f34-clang-asan-ubsan:
+            pre-run: ci/pre.yaml
             requires: CzechLight-deps-f34-clang-asan-ubsan
         - f34-clang-tsan:
+            pre-run: ci/pre.yaml
             requires: CzechLight-deps-f34-clang-tsan
         - f34-gcc-netconf-cli-no-sysrepo:
+            pre-run: ci/pre.yaml
             requires: CzechLight-deps-f34-gcc
         - f34-cpp-coverage-diff:
+            pre-run: ci/pre.yaml
             voting: false
         - clang-format:
             voting: false
         - f34-gcc-cover-previous:
+            pre-run: ci/pre.yaml
             requires: CzechLight-deps-f34-gcc
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f95c615..573e582 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -77,14 +77,14 @@
     message(FATAL_ERROR "Cannot combine ENABLE_FULL_TESTS=ON with BUILD_TESTING=OFF")
 endif()
 
-pkg_check_modules(LIBYANG REQUIRED libyang-cpp>=1.0.248 IMPORTED_TARGET libyang)
-pkg_check_modules(LIBNETCONF2 REQUIRED libnetconf2>=1.1.48 IMPORTED_TARGET libnetconf2)
+pkg_check_modules(LIBYANG-CPP REQUIRED libyang-cpp>=1.0.0 IMPORTED_TARGET)
+pkg_check_modules(LIBNETCONF2 REQUIRED libnetconf2>=2.0.5 IMPORTED_TARGET)
 
 if(ENABLE_FULL_TESTS STREQUAL OFF AND ENABLE_SYSREPO_CLI STREQUAL OFF)
     message(STATUS "Skipping sysrepo per configure options")
     set(SYSREPO_FOUND 0)
 else()
-    pkg_check_modules(SYSREPO sysrepo-cpp>=1.4.148 IMPORTED_TARGET sysrepo)
+    pkg_check_modules(SYSREPO sysrepo-cpp>=1.0.0 IMPORTED_TARGET sysrepo)
 endif()
 
 if(SYSREPO_FOUND)
@@ -165,7 +165,7 @@
         src/sysrepo_access.cpp
         )
 
-    target_link_libraries(sysrepoaccess PUBLIC datastoreaccess ast_values PRIVATE PkgConfig::SYSREPO PkgConfig::LIBYANG)
+    target_link_libraries(sysrepoaccess PUBLIC datastoreaccess ast_values PkgConfig::SYSREPO PRIVATE PkgConfig::LIBYANG-CPP)
 endif()
 
 add_library(netconfaccess STATIC
@@ -173,23 +173,23 @@
     src/netconf_access.cpp
     )
 
-target_link_libraries(netconfaccess PUBLIC datastoreaccess yangschema ast_values utils PkgConfig::LIBNETCONF2 PRIVATE PkgConfig::LIBYANG)
+target_link_libraries(netconfaccess PUBLIC datastoreaccess yangschema ast_values utils PkgConfig::LIBNETCONF2 PRIVATE PkgConfig::LIBYANG-CPP)
 
 add_library(yangaccess STATIC
     src/yang_access.cpp
     )
 
-target_link_libraries(yangaccess PUBLIC datastoreaccess yangschema PRIVATE PkgConfig::LIBYANG)
+target_link_libraries(yangaccess PUBLIC datastoreaccess yangschema PRIVATE PkgConfig::LIBYANG-CPP)
 
 add_library(yangutils STATIC
     src/libyang_utils.cpp
     )
-target_link_libraries(yangutils PUBLIC PkgConfig::LIBYANG)
+target_link_libraries(yangutils PUBLIC PkgConfig::LIBYANG-CPP)
 
 add_library(yangschema STATIC
     src/yang_schema.cpp
     )
-target_link_libraries(yangschema PUBLIC schemas utils yangutils PRIVATE PkgConfig::LIBYANG)
+target_link_libraries(yangschema PUBLIC schemas utils yangutils PRIVATE PkgConfig::LIBYANG-CPP)
 
 add_library(parser STATIC
     src/parser.cpp
@@ -199,7 +199,7 @@
     src/ast_handlers.cpp
     src/completion.cpp
     )
-target_link_libraries(parser schemas utils ast_values)
+target_link_libraries(parser schemas utils ast_values yangutils)
 
 add_library(proxydatastore STATIC
     src/proxy_datastore.cpp
@@ -257,7 +257,7 @@
         tests/wait-a-bit-longer.cpp
         )
     target_include_directories(DoctestIntegration PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/tests/ ${CMAKE_CURRENT_SOURCE_DIR}/src/)
-    target_link_libraries(DoctestIntegration doctest::doctest trompeloeil)
+    target_link_libraries(DoctestIntegration doctest::doctest trompeloeil::trompeloeil)
     target_compile_definitions(DoctestIntegration PUBLIC DOCTEST_CONFIG_SUPER_FAST_ASSERTS)
 
     if(DO_ENABLE_NETCONF_TESTS)
diff --git a/ci/pre.yaml b/ci/pre.yaml
new file mode 100644
index 0000000..4c71e88
--- /dev/null
+++ b/ci/pre.yaml
@@ -0,0 +1,7 @@
+- hosts: all
+  tasks:
+    - name: install pcre2
+      package:
+        name: pcre2-devel
+        state: present
+      become: true
diff --git a/src/cli.cpp b/src/cli.cpp
index fec77d1..24ed37e 100644
--- a/src/cli.cpp
+++ b/src/cli.cpp
@@ -138,6 +138,7 @@
     }
     if (const auto& enableFeatures = args["-e"]) {
         namespace x3 = boost::spirit::x3;
+        std::map<std::string, std::vector<std::string>> toEnable;
         auto grammar = +(x3::char_-":") >> ":" >> +(x3::char_-":");
         for (const auto& enableFeature : enableFeatures.asStringList()) {
             std::pair<std::string, std::string> parsed;
@@ -147,12 +148,15 @@
                 std::cerr << "Error parsing feature enable flags: " << enableFeature << "\n";
                 return 1;
             }
-            try {
-                datastore->enableFeature(parsed.first, parsed.second);
-            } catch (std::runtime_error& ex) {
-                std::cerr << ex.what() << "\n";
-                return 1;
+            toEnable[parsed.first].emplace_back(parsed.second);
+        }
+        try {
+            for (const auto& [moduleName, features] : toEnable) {
+                datastore->setEnabledFeatures(moduleName, features);
             }
+        } catch (std::runtime_error& ex) {
+            std::cerr << ex.what() << "\n";
+            return 1;
         }
     }
 
diff --git a/src/libyang_utils.cpp b/src/libyang_utils.cpp
index a6b22cd..f3881fe 100644
--- a/src/libyang_utils.cpp
+++ b/src/libyang_utils.cpp
@@ -1,130 +1,131 @@
 #include <boost/algorithm/string/predicate.hpp>
 #include <cmath>
+#include <libyang-cpp/Context.hpp>
 #include "datastore_access.hpp"
 #include "libyang_utils.hpp"
 #include "utils.hpp"
 
-leaf_data_ leafValueFromNode(libyang::S_Data_Node_Leaf_List node)
+struct impl_leafValueFromNode {
+    leaf_data_ operator()(const libyang::Empty) const
+    {
+        return empty_{};
+    }
+
+    leaf_data_ operator()(const libyang::Binary& bin) const
+    {
+        return binary_{std::string{bin.base64}};
+    }
+
+    leaf_data_ operator()(const std::vector<libyang::Bit>& bits) const
+    {
+        bits_ res;
+        std::transform(bits.begin(), bits.end(), std::back_inserter(res.m_bits), [] (const libyang::Bit& bit) {
+            return bit.name;
+        });
+        return res;
+    }
+
+    leaf_data_ operator()(const libyang::Enum& enumVal) const
+    {
+        return enum_{enumVal.name};
+    }
+
+    leaf_data_ operator()(const libyang::IdentityRef& identRef) const
+    {
+        return identityRef_{identRef.module, identRef.name};
+    }
+
+    leaf_data_ operator()(const libyang::Decimal64& dec) const
+    {
+        return dec.number * std::pow(10, -dec.digits);
+    }
+
+    leaf_data_ operator()(const std::optional<libyang::DataNode>&) const
+    {
+        throw std::runtime_error("instance-identifier is not supported");
+    }
+
+    template <typename Type>
+    leaf_data_ operator()(const Type& val) const
+    {
+        return val;
+    }
+};
+
+leaf_data_ leafValueFromNode(libyang::DataNodeTerm node)
 {
-    std::function<leaf_data_(libyang::S_Data_Node_Leaf_List)> impl = [&impl](libyang::S_Data_Node_Leaf_List node) -> leaf_data_ {
-        // value_type() is what's ACTUALLY stored inside `node`
-        // Leafrefs sometimes don't hold a reference to another, but they have the actual pointed-to value.
-        switch (node->value_type()) {
-        case LY_TYPE_ENUM:
-            return enum_{node->value()->enm()->name()};
-        case LY_TYPE_UINT8:
-            return node->value()->uint8();
-        case LY_TYPE_UINT16:
-            return node->value()->uint16();
-        case LY_TYPE_UINT32:
-            return node->value()->uint32();
-        case LY_TYPE_UINT64:
-            return node->value()->uint64();
-        case LY_TYPE_INT8:
-            return node->value()->int8();
-        case LY_TYPE_INT16:
-            return node->value()->int16();
-        case LY_TYPE_INT32:
-            return node->value()->int32();
-        case LY_TYPE_INT64:
-            return node->value()->int64();
-        case LY_TYPE_DEC64: {
-            auto v = node->value()->dec64();
-            return v.value * std::pow(10, -v.digits);
-        }
-        case LY_TYPE_BOOL:
-            return node->value()->bln();
-        case LY_TYPE_STRING:
-            return std::string{node->value()->string()};
-        case LY_TYPE_BINARY:
-            return binary_{node->value()->binary()};
-        case LY_TYPE_IDENT:
-            return identityRef_{node->value()->ident()->module()->name(), node->value()->ident()->name()};
-        case LY_TYPE_EMPTY:
-            return empty_{};
-        case LY_TYPE_LEAFREF: {
-            auto refsTo = node->value()->leafref();
-            assert(refsTo);
-            return impl(std::make_shared<libyang::Data_Node_Leaf_List>(node->value()->leafref()));
-        }
-        case LY_TYPE_BITS: {
-            auto bits = node->value()->bit();
-            std::vector<libyang::S_Type_Bit> filterNull;
-            std::copy_if(bits.begin(), bits.end(), std::back_inserter(filterNull), [](auto bit) { return bit; });
-            bits_ res;
-            std::transform(filterNull.begin(), filterNull.end(), std::inserter(res.m_bits, res.m_bits.end()), [](const auto& bit) { return bit->name(); });
-            return bits_{res};
-        }
-        default:
-            return std::string{"(can't print)"};
-        }
-    };
-    return impl(node);
+    return std::visit(impl_leafValueFromNode{},node.value());
 }
 
 namespace {
-void impl_lyNodesToTree(DatastoreAccess::Tree& res, const std::vector<std::shared_ptr<libyang::Data_Node>> items, std::optional<std::string> ignoredXPathPrefix)
+template <typename CollectionType>
+void impl_lyNodesToTree(DatastoreAccess::Tree& res, CollectionType items, std::optional<std::string> ignoredXPathPrefix)
 {
     auto stripXPathPrefix = [&ignoredXPathPrefix](auto path) {
         return ignoredXPathPrefix && path.find(*ignoredXPathPrefix) != std::string::npos ? path.substr(ignoredXPathPrefix->size()) : path;
     };
 
     for (const auto& it : items) {
-        if (it->schema()->nodetype() == LYS_CONTAINER) {
-            if (libyang::Schema_Node_Container{it->schema()}.presence()) {
+        if (it.schema().nodeType() == libyang::NodeType::Container) {
+            if (it.schema().asContainer().isPresence()) {
                 // The fact that the container is included in the data tree
                 // means that it is present and I don't need to check any
                 // value.
-                res.emplace_back(stripXPathPrefix(it->path()), special_{SpecialValue::PresenceContainer});
+                res.emplace_back(stripXPathPrefix(std::string{it.path()}), special_{SpecialValue::PresenceContainer});
             }
         }
-        if (it->schema()->nodetype() == LYS_LIST) {
-            res.emplace_back(stripXPathPrefix(it->path()), special_{SpecialValue::List});
+        if (it.schema().nodeType() == libyang::NodeType::List) {
+            res.emplace_back(stripXPathPrefix(std::string{it.path()}), special_{SpecialValue::List});
         }
-        if (it->schema()->nodetype() == LYS_LEAF || it->schema()->nodetype() == LYS_LEAFLIST) {
-            auto leaf = std::make_shared<libyang::Data_Node_Leaf_List>(it);
-            auto value = leafValueFromNode(leaf);
-            res.emplace_back(stripXPathPrefix(it->path()), value);
+        if (it.schema().nodeType() == libyang::NodeType::Leaf || it.schema().nodeType() == libyang::NodeType::Leaflist) {
+            auto term = it.asTerm();
+            auto value = leafValueFromNode(term);
+            res.emplace_back(stripXPathPrefix(std::string{it.path()}), value);
         }
     }
 }
 }
 
-void lyNodesToTree(DatastoreAccess::Tree& res, const std::vector<std::shared_ptr<libyang::Data_Node>> items, std::optional<std::string> ignoredXPathPrefix)
+template <typename CollectionType>
+void lyNodesToTree(DatastoreAccess::Tree& res, CollectionType items, std::optional<std::string> ignoredXPathPrefix)
 {
-    for (auto it = items.begin(); it < items.end(); it++) {
-        if ((*it)->schema()->nodetype() == LYS_LEAFLIST) {
-            auto leafListPath = stripLeafListValueFromPath((*it)->path());
+    for (auto it = items.begin(); it != items.end(); /* nothing */) {
+        if ((*it).schema().nodeType() == libyang::NodeType::Leaflist) {
+            auto leafListPath = stripLeafListValueFromPath(std::string{(*it).path()});
             res.emplace_back(leafListPath, special_{SpecialValue::LeafList});
-            while (it != items.end() && boost::starts_with((*it)->path(), leafListPath)) {
-                impl_lyNodesToTree(res, (*it)->tree_dfs(), ignoredXPathPrefix);
+            while (it != items.end() && boost::starts_with(std::string{(*it).path()}, leafListPath)) {
+                impl_lyNodesToTree(res, it->childrenDfs(), ignoredXPathPrefix);
                 it++;
             }
         } else {
-            impl_lyNodesToTree(res, (*it)->tree_dfs(), ignoredXPathPrefix);
+            impl_lyNodesToTree(res, it->childrenDfs(), ignoredXPathPrefix);
+            it++;
         }
     }
 }
 
-DatastoreAccess::Tree rpcOutputToTree(const std::string& rpcPath, libyang::S_Data_Node output)
+using SiblingColl = libyang::Collection<libyang::DataNode, libyang::IterationType::Sibling>;
+using DfsColl = libyang::Collection<libyang::DataNode, libyang::IterationType::Dfs>;
+
+template
+void lyNodesToTree<SiblingColl>(DatastoreAccess::Tree& res, SiblingColl items, std::optional<std::string> ignoredXPathPrefix);
+template
+void lyNodesToTree<DfsColl>(DatastoreAccess::Tree& res, DfsColl items, std::optional<std::string> ignoredXPathPrefix);
+template
+void lyNodesToTree<libyang::Set<libyang::DataNode>>(DatastoreAccess::Tree& res, libyang::Set<libyang::DataNode> items, std::optional<std::string> ignoredXPathPrefix);
+
+DatastoreAccess::Tree rpcOutputToTree(libyang::DataNode output)
 {
     DatastoreAccess::Tree res;
-    if (output) {
-        // The output is "some top-level node". If we actually want the output of our RPC/action we need to use
-        // find_path.  Also, our `path` is fully prefixed, but the output paths aren't. So we use outputNode->path() to
-        // get the unprefixed path.
-
-        auto outputNode = output->find_path(rpcPath.c_str())->data().front();
-        lyNodesToTree(res, {outputNode}, joinPaths(outputNode->path(), "/"));
-    }
+    lyNodesToTree(res, output.siblings(), joinPaths(std::string{output.path()}, "/"));
     return res;
 }
 
-libyang::S_Data_Node treeToRpcInput(libyang::S_Context ctx, const std::string& path, DatastoreAccess::Tree in)
+libyang::DataNode treeToRpcInput(libyang::Context ctx, const std::string& path, DatastoreAccess::Tree in)
 {
-    auto root = std::make_shared<libyang::Data_Node>(ctx, path.c_str(), nullptr, LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_UPDATE);
+    auto root = ctx.newPath(path.c_str(), nullptr, libyang::CreationOptions::Update);
     for (const auto& [k, v] : in) {
-        root->new_path(ctx, k.c_str(), leafDataToString(v).c_str(), LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_UPDATE);
+        root.newPath(k.c_str(), leafDataToString(v).c_str(), libyang::CreationOptions::Update);
     }
 
     return root;
diff --git a/src/libyang_utils.hpp b/src/libyang_utils.hpp
index b5f5e96..8a470c7 100644
--- a/src/libyang_utils.hpp
+++ b/src/libyang_utils.hpp
@@ -5,11 +5,12 @@
  *
 */
 
-#include <libyang/Tree_Data.hpp>
+#include <libyang-cpp/DataNode.hpp>
 #include "ast_values.hpp"
 #include "datastore_access.hpp"
 
-leaf_data_ leafValueFromNode(libyang::S_Data_Node_Leaf_List node);
-void lyNodesToTree(DatastoreAccess::Tree& res, const std::vector<std::shared_ptr<libyang::Data_Node>> items, std::optional<std::string> ignoredXPathPrefix = std::nullopt);
-libyang::S_Data_Node treeToRpcInput(libyang::S_Context ctx, const std::string& path, DatastoreAccess::Tree in);
-DatastoreAccess::Tree rpcOutputToTree(const std::string& rpcPath, libyang::S_Data_Node output);
+leaf_data_ leafValueFromNode(libyang::DataNodeTerm node);
+template <typename CollectionType>
+void lyNodesToTree(DatastoreAccess::Tree& res, CollectionType items, std::optional<std::string> ignoredXPathPrefix = std::nullopt);
+libyang::DataNode treeToRpcInput(libyang::Context ctx, const std::string& path, DatastoreAccess::Tree in);
+DatastoreAccess::Tree rpcOutputToTree(libyang::DataNode output);
diff --git a/src/netconf-client.cpp b/src/netconf-client.cpp
index 4638bcb..0685db6 100644
--- a/src/netconf-client.cpp
+++ b/src/netconf-client.cpp
@@ -7,7 +7,8 @@
 */
 
 #include <cstring>
-#include <libyang/Tree_Data.hpp>
+#include <libyang-cpp/Context.hpp>
+#include <libyang-cpp/DataNode.hpp>
 #include <mutex>
 extern "C" {
 #include <nc_client.h>
@@ -58,15 +59,6 @@
 
 static std::mutex clientOptions;
 
-inline void custom_free_nc_reply_data(nc_reply_data* reply)
-{
-    nc_reply_free(reinterpret_cast<nc_reply*>(reply));
-}
-inline void custom_free_nc_reply_error(nc_reply_error* reply)
-{
-    nc_reply_free(reinterpret_cast<nc_reply*>(reply));
-}
-
 char* ssh_auth_interactive_cb(const char* auth_name, const char* instruction, const char* prompt, int echo, void* priv)
 {
     const auto cb = static_cast<const client::KbdInteractiveCb*>(priv);
@@ -74,24 +66,19 @@
     return ::strdup(res.c_str());
 }
 
-template <typename Type> using deleter_type_for = void (*)(Type*);
-template <typename Type> deleter_type_for<Type> const deleter_for;
-
-template <> const auto deleter_for<nc_rpc> = nc_rpc_free;
-template <> const auto deleter_for<nc_reply> = nc_reply_free;
-template <> const auto deleter_for<nc_reply_data> = custom_free_nc_reply_data;
-template <> const auto deleter_for<nc_reply_error> = custom_free_nc_reply_error;
-
-template <typename T>
-using unique_ptr_for = std::unique_ptr<T, decltype(deleter_for<T>)>;
-
-template <typename T>
-auto guarded(T* ptr)
+auto guarded(nc_rpc* ptr)
 {
-    return unique_ptr_for<T>(ptr, deleter_for<T>);
+    return std::unique_ptr<nc_rpc, decltype(&nc_rpc_free)>(ptr, nc_rpc_free);
 }
 
-unique_ptr_for<struct nc_reply> do_rpc(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
+namespace {
+const auto getData_path = "/ietf-netconf-nmda:get-data/data";
+const auto get_path = "/ietf-netconf:get/data";
+}
+
+using managed_rpc = std::invoke_result_t<decltype(guarded), nc_rpc*>;
+
+std::optional<libyang::DataNode> do_rpc(client::Session* session, managed_rpc&& rpc, const char* dataIdentifier)
 {
     uint64_t msgid;
     NC_MSG_TYPE msgtype;
@@ -104,11 +91,11 @@
         throw std::runtime_error{"Timeout sending an RPC"};
     }
 
-    struct nc_reply* raw_reply;
+    lyd_node* raw_reply;
+    lyd_node* envp;
     while (true) {
-        msgtype = nc_recv_reply(session->session_internal(), rpc.get(), msgid, 20000, LYD_OPT_DESTRUCT | LYD_OPT_NOSIBLINGS, &raw_reply);
-        auto reply = guarded(raw_reply);
-        raw_reply = nullptr;
+        msgtype = nc_recv_reply(session->session_internal(), rpc.get(), msgid, 20000, &envp, &raw_reply);
+        auto replyInfo = libyang::wrapRawNode(envp);
 
         switch (msgtype) {
         case NC_MSG_ERROR:
@@ -120,76 +107,75 @@
         case NC_MSG_NOTIF:
             continue;
         default:
-            return reply;
+            if (!raw_reply) { // <ok> reply, or empty data node, or error
+                std::string msg;
+                for (const auto& child : replyInfo.child()->siblings()) {
+                    if (child.asOpaque().name().name == "rpc-error") {
+                        for (const auto& error : child.childrenDfs()) {
+                            if (error.asOpaque().name().name == "error-message") {
+                                msg += "Error: ";
+                                msg += error.asOpaque().value();
+                            }
+
+                            if (error.asOpaque().name().name == "error-path") {
+                                msg += "Path: ";
+                                msg += error.asOpaque().value();
+                            }
+
+                            if (error.asOpaque().name().name == "error-type") {
+                                msg += "Type: ";
+                                msg += error.asOpaque().value();
+                            }
+
+                            if (error.asOpaque().name().name == "error-tag") {
+                                msg += "Tag: ";
+                                msg += error.asOpaque().value();
+                            }
+
+                            if (error.asOpaque().name().name == "error-app-tag") {
+                                msg += "App-tag: ";
+                                msg += error.asOpaque().value();
+                            }
+                        }
+
+                        msg += "\n";
+                    }
+                }
+
+                if (!msg.empty()) {
+                    throw client::ReportedError{msg};
+                }
+
+                return std::nullopt;
+            }
+            auto wrapped = libyang::wrapRawNode(raw_reply);
+
+            // If we have a dataIdentifier, then we'll need to look for it.
+            // Some operations don't have that, and then the result data are just the wrapped node.
+            if (!dataIdentifier) {
+                return wrapped;
+            }
+
+            auto anydataValue = wrapped.findPath(dataIdentifier, libyang::OutputNodes::Yes)->asAny().releaseValue();
+
+            // If there's no anydata value, then that means we get empty (but valid) data.
+            if (!anydataValue) {
+                return std::nullopt;
+            }
+
+            return std::get<libyang::DataNode>(*anydataValue);
         }
     }
     __builtin_unreachable();
 }
 
-client::ReportedError make_error(unique_ptr_for<struct nc_reply>&& reply)
+void do_rpc_ok(client::Session* session, managed_rpc&& rpc)
 {
-    if (reply->type != NC_RPL_ERROR) {
-        throw std::logic_error{"Cannot extract an error from a non-error server reply"};
-    }
-
-    auto errorReply = guarded(reinterpret_cast<struct nc_reply_error*>(reply.release()));
-
-    // TODO: capture the error details, not just that human-readable string
-    std::ostringstream ss;
-    ss << "Server error:" << std::endl;
-    for (uint32_t i = 0; i < errorReply->count; ++i) {
-        const auto e = errorReply->err[i];
-        ss << " #" << i << ": " << e.message;
-        if (e.path) {
-            ss << " (XPath " << e.path << ")";
-        }
-        ss << std::endl;
-    }
-    return client::ReportedError{ss.str()};
-}
-
-std::optional<unique_ptr_for<struct nc_reply_data>> do_rpc_data_or_ok(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
-{
-    auto x = do_rpc(session, std::move(rpc));
-
-    switch (x->type) {
-    case NC_RPL_DATA:
-        return guarded(reinterpret_cast<struct nc_reply_data*>(x.release()));
-    case NC_RPL_OK:
-        return std::nullopt;
-    case NC_RPL_ERROR:
-        throw make_error(std::move(x));
-    default:
-        throw std::runtime_error{"Unhandled reply type"};
-    }
-}
-
-unique_ptr_for<struct nc_reply_data> do_rpc_data(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
-{
-    auto x = do_rpc_data_or_ok(session, std::move(rpc));
-    if (!x) {
-        throw std::runtime_error{"Received OK instead of a data reply"};
-    }
-    return std::move(*x);
-}
-
-void do_rpc_ok(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
-{
-    auto x = do_rpc_data_or_ok(session, std::move(rpc));
+    auto x = do_rpc(session, std::move(rpc), nullptr);
     if (x) {
         throw std::runtime_error{"Unexpected DATA reply"};
     }
 }
-
-std::shared_ptr<libyang::Data_Node> do_get(client::Session* session, unique_ptr_for<nc_rpc> rpc)
-{
-    auto reply = impl::do_rpc_data(session, std::move(rpc));
-    auto dataNode = libyang::create_new_Data_Node(reply->data);
-    // TODO: can we do without copying?
-    // If we just default-construct a new node (or use the create_new_Data_Node) and then set reply->data to nullptr,
-    // there are mem leaks and even libnetconf2 complains loudly.
-    return dataNode ? dataNode->dup_withsiblings(1) : nullptr;
-}
 }
 
 namespace client {
@@ -210,9 +196,9 @@
     return m_session;
 }
 
-libyang::S_Context Session::libyangContext()
+libyang::Context Session::libyangContext()
 {
-    return std::make_shared<libyang::Context>(nc_session_get_ctx(m_session), nullptr);
+    return libyang::createUnmanagedContext(const_cast<ly_ctx*>(nc_session_get_ctx(m_session)), nullptr);
 }
 
 Session::Session(struct nc_session* session)
@@ -298,13 +284,13 @@
     return res;
 }
 
-std::shared_ptr<libyang::Data_Node> Session::get(const std::optional<std::string>& filter)
+std::optional<libyang::DataNode> Session::get(const std::optional<std::string>& filter)
 {
     auto rpc = impl::guarded(nc_rpc_get(filter ? filter->c_str() : nullptr, NC_WD_ALL, NC_PARAMTYPE_CONST));
     if (!rpc) {
         throw std::runtime_error("Cannot create get RPC");
     }
-    return impl::do_get(this, std::move(rpc));
+    return impl::do_rpc(this, std::move(rpc), impl::get_path);
 }
 
 const char* datastoreToString(NmdaDatastore datastore)
@@ -322,13 +308,13 @@
     __builtin_unreachable();
 }
 
-std::shared_ptr<libyang::Data_Node> Session::getData(const NmdaDatastore datastore, const std::optional<std::string>& filter)
+std::optional<libyang::DataNode> Session::getData(const NmdaDatastore datastore, const std::optional<std::string>& filter)
 {
     auto rpc = impl::guarded(nc_rpc_getdata(datastoreToString(datastore), filter ? filter->c_str() : nullptr, nullptr, nullptr, 0, 0, 0, 0, NC_WD_ALL, NC_PARAMTYPE_CONST));
     if (!rpc) {
         throw std::runtime_error("Cannot create get RPC");
     }
-    return impl::do_get(this, std::move(rpc));
+    return impl::do_rpc(this, std::move(rpc), impl::getData_path);
 }
 
 void Session::editData(const NmdaDatastore datastore, const std::string& data)
@@ -340,34 +326,6 @@
     return impl::do_rpc_ok(this, std::move(rpc));
 }
 
-std::string Session::getSchema(const std::string_view identifier, const std::optional<std::string_view> version)
-{
-    auto rpc = impl::guarded(nc_rpc_getschema(identifier.data(), version ? version.value().data() : nullptr, nullptr, NC_PARAMTYPE_CONST));
-    if (!rpc) {
-        throw std::runtime_error("Cannot create get-schema RPC");
-    }
-    auto reply = impl::do_rpc_data(this, std::move(rpc));
-
-    auto node = libyang::create_new_Data_Node(reply->data)->dup_withsiblings(1);
-    auto set = node->find_path("data");
-    for (auto node : set->data()) {
-        if (node->schema()->nodetype() == LYS_ANYXML) {
-            libyang::Data_Node_Anydata anydata(node);
-            return anydata.value().str;
-        }
-    }
-    throw std::runtime_error("Got a reply to get-schema, but it didn't contain the required schema");
-}
-
-std::shared_ptr<libyang::Data_Node> Session::getConfig(const NC_DATASTORE datastore, const std::optional<const std::string> filter)
-{
-    auto rpc = impl::guarded(nc_rpc_getconfig(datastore, filter ? filter->c_str() : nullptr, NC_WD_ALL, NC_PARAMTYPE_CONST));
-    if (!rpc) {
-        throw std::runtime_error("Cannot create get-config RPC");
-    }
-    return impl::do_get(this, std::move(rpc));
-}
-
 void Session::editConfig(const NC_DATASTORE datastore,
                          const NC_RPC_EDIT_DFLTOP defaultOperation,
                          const NC_RPC_EDIT_TESTOPT testOption,
@@ -408,19 +366,14 @@
     impl::do_rpc_ok(this, std::move(rpc));
 }
 
-std::shared_ptr<libyang::Data_Node> Session::rpc_or_action(const std::string& xmlData)
+std::optional<libyang::DataNode> Session::rpc_or_action(const std::string& xmlData)
 {
     auto rpc = impl::guarded(nc_rpc_act_generic_xml(xmlData.c_str(), NC_PARAMTYPE_CONST));
     if (!rpc) {
         throw std::runtime_error("Cannot create generic RPC");
     }
-    auto reply = impl::do_rpc_data_or_ok(this, std::move(rpc));
-    if (reply) {
-        auto dataNode = libyang::create_new_Data_Node((*reply)->data);
-        return dataNode->dup_withsiblings(1);
-    } else {
-        return nullptr;
-    }
+
+    return impl::do_rpc(this, std::move(rpc), nullptr);
 }
 
 void Session::copyConfig(const NC_DATASTORE source, const NC_DATASTORE destination)
diff --git a/src/netconf-client.hpp b/src/netconf-client.hpp
index 839c528..5a16cf2 100644
--- a/src/netconf-client.hpp
+++ b/src/netconf-client.hpp
@@ -12,8 +12,8 @@
 struct nc_session;
 
 namespace libyang {
-class Data_Node;
 class Context;
+class DataNode;
 }
 
 namespace libnetconf {
@@ -46,11 +46,8 @@
     static std::unique_ptr<Session> connectSocket(const std::string& path);
     static std::unique_ptr<Session> connectFd(const int source, const int sink);
     [[nodiscard]] std::vector<std::string_view> capabilities() const;
-    std::shared_ptr<libyang::Data_Node> getConfig(const NC_DATASTORE datastore,
-                                                  const std::optional<const std::string> filter = std::nullopt); // TODO: arguments...
-    std::shared_ptr<libyang::Data_Node> get(const std::optional<std::string>& filter = std::nullopt);
-    std::shared_ptr<libyang::Data_Node> getData(const NmdaDatastore datastore, const std::optional<std::string>& filter = std::nullopt);
-    std::string getSchema(const std::string_view identifier, const std::optional<std::string_view> version);
+    std::optional<libyang::DataNode> get(const std::optional<std::string>& filter = std::nullopt);
+    std::optional<libyang::DataNode> getData(const NmdaDatastore datastore, const std::optional<std::string>& filter = std::nullopt);
     void editConfig(const NC_DATASTORE datastore,
                     const NC_RPC_EDIT_DFLTOP defaultOperation,
                     const NC_RPC_EDIT_TESTOPT testOption,
@@ -58,12 +55,12 @@
                     const std::string& data);
     void editData(const NmdaDatastore datastore, const std::string& data);
     void copyConfigFromString(const NC_DATASTORE target, const std::string& data);
-    std::shared_ptr<libyang::Data_Node> rpc_or_action(const std::string& xmlData);
+    std::optional<libyang::DataNode> rpc_or_action(const std::string& xmlData);
     void copyConfig(const NC_DATASTORE source, const NC_DATASTORE destination);
     void commit();
     void discard();
 
-    std::shared_ptr<libyang::Context> libyangContext();
+    libyang::Context libyangContext();
     struct nc_session* session_internal(); // FIXME: remove me
 protected:
     struct nc_session* m_session;
diff --git a/src/netconf_access.cpp b/src/netconf_access.cpp
index a6dbb7c..2b76e50 100644
--- a/src/netconf_access.cpp
+++ b/src/netconf_access.cpp
@@ -5,8 +5,6 @@
  *
 */
 
-#include <libyang/Libyang.hpp>
-#include <libyang/Tree_Data.hpp>
 #include "libyang_utils.hpp"
 #include "netconf-client.hpp"
 #include "netconf_access.hpp"
@@ -55,7 +53,7 @@
     }();
 
     if (config) {
-        lyNodesToTree(res, config->tree_for());
+        lyNodesToTree(res, config->siblings());
     }
     return res;
 }
@@ -107,22 +105,28 @@
 void NetconfAccess::setLeaf(const std::string& path, leaf_data_ value)
 {
     auto lyValue = value.type() == typeid(empty_) ? std::nullopt : std::optional(leafDataToString(value));
-    auto node = m_schema->dataNodeFromPath(path, lyValue);
-    doEditFromDataNode(node);
+    auto nodes = m_schema->dataNodeFromPath(path, lyValue);
+    doEditFromDataNode(*nodes.createdParent);
 }
 
 void NetconfAccess::createItem(const std::string& path)
 {
-    auto node = m_schema->dataNodeFromPath(path);
-    doEditFromDataNode(node);
+    auto nodes = m_schema->dataNodeFromPath(path);
+    doEditFromDataNode(*nodes.createdParent);
 }
 
 void NetconfAccess::deleteItem(const std::string& path)
 {
-    auto node = m_schema->dataNodeFromPath(path);
-    auto container = *(node->find_path(path.c_str())->data().begin());
-    container->insert_attr(m_schema->getYangModule("ietf-netconf"), "operation", "delete");
-    doEditFromDataNode(node);
+    auto nodes = m_schema->dataNodeFromPath(path);
+
+    // When deleting leafs, `nodes.newNode` is opaque, because the leaf does not have a value. We need to use
+    // newAttrOpaqueJSON for opaque leafs.
+    if (nodes.createdNode->isOpaque()) {
+        nodes.createdNode->newAttrOpaqueJSON("ietf-netconf", "ietf-netconf:operation", "delete");
+    } else {
+        nodes.createdNode->newMeta(*m_schema->getYangModule("ietf-netconf"), "operation", "delete");
+    }
+    doEditFromDataNode(*nodes.createdParent);
 }
 
 struct impl_toYangInsert {
@@ -143,29 +147,29 @@
 
 void NetconfAccess::moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move)
 {
-    auto node = m_schema->dataNodeFromPath(source);
-    auto sourceNode = *(node->find_path(source.c_str())->data().begin());
-    auto yangModule = m_schema->getYangModule("yang");
-    sourceNode->insert_attr(yangModule, "insert", toYangInsert(move).c_str());
+    auto nodes = m_schema->dataNodeFromPath(source);
+    auto sourceNode = *(nodes.createdNode->findPath(source.c_str()));
+    auto yangModule = *m_schema->getYangModule("yang");
+    sourceNode.newMeta(yangModule, "insert", toYangInsert(move).c_str());
 
     if (std::holds_alternative<yang::move::Relative>(move)) {
         auto relative = std::get<yang::move::Relative>(move);
         if (m_schema->nodeType(source) == yang::NodeTypes::LeafList) {
-            sourceNode->insert_attr(yangModule, "value", leafDataToString(relative.m_path.at(".")).c_str());
+            sourceNode.newMeta(yangModule, "value", leafDataToString(relative.m_path.at(".")).c_str());
         } else {
-            sourceNode->insert_attr(yangModule, "key", instanceToString(relative.m_path, node->node_module()->name()).c_str());
+            sourceNode.newMeta(yangModule, "key", instanceToString(relative.m_path, std::string{nodes.createdNode->schema().module().name()}).c_str());
         }
     }
     doEditFromDataNode(sourceNode);
 }
 
-void NetconfAccess::doEditFromDataNode(std::shared_ptr<libyang::Data_Node> dataNode)
+void NetconfAccess::doEditFromDataNode(libyang::DataNode dataNode)
 {
-    auto data = dataNode->print_mem(LYD_XML, 0);
+    auto data = dataNode.printStr(libyang::DataFormat::XML, libyang::PrintFlags::WithSiblings);
     if (m_serverHasNMDA) {
-        m_session->editData(targetToDs_set(m_target), data);
+        m_session->editData(targetToDs_set(m_target), std::string{*data});
     } else {
-        m_session->editConfig(NC_DATASTORE_CANDIDATE, NC_RPC_EDIT_DFLTOP_MERGE, NC_RPC_EDIT_TESTOPT_TESTSET, NC_RPC_EDIT_ERROPT_STOP, data);
+        m_session->editConfig(NC_DATASTORE_CANDIDATE, NC_RPC_EDIT_DFLTOP_MERGE, NC_RPC_EDIT_TESTOPT_TESTSET, NC_RPC_EDIT_ERROPT_STOP, std::string{*data});
     }
 }
 
@@ -182,10 +186,13 @@
 DatastoreAccess::Tree NetconfAccess::execute(const std::string& path, const Tree& input)
 {
     auto inputNode = treeToRpcInput(m_session->libyangContext(), path, input);
-    auto data = inputNode->print_mem(LYD_XML, 0);
+    auto data = inputNode.printStr(libyang::DataFormat::XML, libyang::PrintFlags::WithSiblings);
 
-    auto output = m_session->rpc_or_action(data);
-    return rpcOutputToTree(path, output);
+    auto output = m_session->rpc_or_action(std::string{*data});
+    if (!output) {
+        return {};
+    }
+    return rpcOutputToTree(*output);
 }
 
 NC_DATASTORE toNcDatastore(Datastore datastore)
@@ -212,41 +219,41 @@
 std::vector<ListInstance> NetconfAccess::listInstances(const std::string& path)
 {
     std::vector<ListInstance> res;
-    auto list = m_schema->dataNodeFromPath(path);
+    auto keys = m_session->libyangContext().findXPath(path.c_str()).front().asList().keys();
+    auto nodes = m_session->libyangContext().newPath2(path.c_str(), nullptr, libyang::CreationOptions::Opaque);
 
-    // This inserts selection nodes - I only want keys not other data
-    // To get the keys, I have to call find_path here - otherwise I would get keys of a top-level node (which might not even be a list)
-    auto keys = libyang::Schema_Node_List{(*(list->find_path(path.c_str())->data().begin()))->schema()}.keys();
+    // Here we create a tree with "selection leafs" for all they keys of our wanted list. These leafs tell NETCONF, that
+    // we only want the list's keys and not any other data.
     for (const auto& keyLeaf : keys) {
-        // Have to call find_path here - otherwise I'll have the list, not the leaf inside it
-        auto selectionLeaf = *(m_schema->dataNodeFromPath(keyLeaf->path())->find_path(keyLeaf->path().c_str())->data().begin());
-        auto actualList = *(list->find_path(path.c_str())->data().begin());
-        actualList->insert(selectionLeaf);
+        // Selection leafs need to be inserted directly to the list using relative paths, that's why `newNode` is used
+        // here.
+        nodes.createdNode->newPath(keyLeaf.name().data(), nullptr, libyang::CreationOptions::Opaque);
     }
 
-    auto instances = m_session->get(list->print_mem(LYD_XML, 0));
+    // Have to use `newParent` in case our wanted list is a nested list. With `newNode` I would only send the inner
+    // nested list and not the whole tree.
+    auto instances = m_session->get(std::string{*nodes.createdParent->printStr(libyang::DataFormat::XML, libyang::PrintFlags::WithSiblings)});
 
     if (!instances) {
         return res;
     }
 
-    for (const auto& instance : instances->find_path(path.c_str())->data()) {
+    for (const auto& instance : instances->findXPath(path.c_str())) {
         ListInstance instanceRes;
 
-        // I take the first child here, because the first element (the parent of the child()) will be the list
-        for (const auto& keyLeaf : instance->child()->tree_for()) {
+        for (const auto& keyLeaf : instance.child()->siblings()) {
             // FIXME: even though we specified we only want the key leafs, Netopeer disregards that and sends more data,
             // even lists and other stuff. We only want keys, so filter out non-leafs and non-keys
             // https://github.com/CESNET/netopeer2/issues/825
-            if (keyLeaf->schema()->nodetype() != LYS_LEAF) {
+            if (keyLeaf.schema().nodeType() != libyang::NodeType::Leaf) {
                 continue;
             }
-            if (!std::make_shared<libyang::Schema_Node_Leaf>(keyLeaf->schema())->is_key()) {
+            if (!keyLeaf.schema().asLeaf().isKey()) {
                 continue;
             }
 
-            auto leafData = std::make_shared<libyang::Data_Node_Leaf_List>(keyLeaf);
-            instanceRes.insert({leafData->schema()->name(), leafValueFromNode(leafData)});
+            auto leafData = keyLeaf.asTerm();
+            instanceRes.insert({std::string{leafData.schema().name()}, leafValueFromNode(leafData)});
         }
         res.emplace_back(instanceRes);
     }
@@ -260,5 +267,10 @@
     if (!config) {
         return "";
     }
-    return config->print_mem(format == DataFormat::Xml ? LYD_XML : LYD_JSON, LYP_WITHSIBLINGS | LYP_FORMAT);
+    auto str = config->printStr(format == DataFormat::Xml ? libyang::DataFormat::XML : libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings);
+    if (!str) {
+        return "";
+    }
+
+    return std::string{*str};
 }
diff --git a/src/netconf_access.hpp b/src/netconf_access.hpp
index 43ca1e8..eb678d5 100644
--- a/src/netconf_access.hpp
+++ b/src/netconf_access.hpp
@@ -21,10 +21,6 @@
 }
 }
 
-namespace libyang {
-class Data_Node;
-}
-
 class Schema;
 class YangSchema;
 
@@ -57,7 +53,7 @@
 private:
     std::vector<ListInstance> listInstances(const std::string& path) override;
 
-    void doEditFromDataNode(std::shared_ptr<libyang::Data_Node> dataNode);
+    void doEditFromDataNode(libyang::DataNode dataNode);
 
     void checkNMDA();
 
diff --git a/src/sysrepo_access.cpp b/src/sysrepo_access.cpp
index bbdf6d1..34d255a 100644
--- a/src/sysrepo_access.cpp
+++ b/src/sysrepo_access.cpp
@@ -7,86 +7,34 @@
 */
 
 #include <experimental/iterator>
-#include <libyang/Tree_Data.hpp>
-#include <libyang/Tree_Schema.hpp>
 #include <sstream>
 #include <sysrepo-cpp/Session.hpp>
+#include <sysrepo-cpp/utils/exception.hpp>
 #include "libyang_utils.hpp"
 #include "sysrepo_access.hpp"
 #include "utils.hpp"
 #include "yang_schema.hpp"
 
-const auto OPERATION_TIMEOUT_MS = 1000;
-
-struct valFromValue : boost::static_visitor<sysrepo::S_Val> {
-    sysrepo::S_Val operator()(const enum_& value) const
-    {
-        return std::make_shared<sysrepo::Val>(value.m_value.c_str(), SR_ENUM_T);
-    }
-
-    sysrepo::S_Val operator()(const binary_& value) const
-    {
-        return std::make_shared<sysrepo::Val>(value.m_value.c_str(), SR_BINARY_T);
-    }
-
-    sysrepo::S_Val operator()(const empty_) const
-    {
-        return std::make_shared<sysrepo::Val>(nullptr, SR_LEAF_EMPTY_T);
-    }
-
-    sysrepo::S_Val operator()(const identityRef_& value) const
-    {
-        auto res = value.m_prefix ? (value.m_prefix.value().m_name + ":" + value.m_value) : value.m_value;
-        return std::make_shared<sysrepo::Val>(res.c_str(), SR_IDENTITYREF_T);
-    }
-
-    sysrepo::S_Val operator()(const special_& value) const
-    {
-        throw std::runtime_error("Tried constructing S_Val from a " + specialValueToString(value));
-    }
-
-    sysrepo::S_Val operator()(const std::string& value) const
-    {
-        return std::make_shared<sysrepo::Val>(value.c_str());
-    }
-
-    sysrepo::S_Val operator()(const bits_& value) const
-    {
-        std::stringstream ss;
-        std::copy(value.m_bits.begin(), value.m_bits.end(), std::experimental::make_ostream_joiner(ss, " "));
-        return std::make_shared<sysrepo::Val>(ss.str().c_str(), SR_BITS_T);
-    }
-
-    template <typename T>
-    sysrepo::S_Val operator()(const T& value) const
-    {
-        return std::make_shared<sysrepo::Val>(value);
-    }
-};
+const auto OPERATION_TIMEOUT_MS = std::chrono::milliseconds{1000};
 
 SysrepoAccess::~SysrepoAccess() = default;
 
-sr_datastore_t toSrDatastore(Datastore datastore)
+sysrepo::Datastore toSrDatastore(Datastore datastore)
 {
     switch (datastore) {
     case Datastore::Running:
-        return SR_DS_RUNNING;
+        return sysrepo::Datastore::Running;
     case Datastore::Startup:
-        return SR_DS_STARTUP;
+        return sysrepo::Datastore::Startup;
     }
     __builtin_unreachable();
 }
 
 SysrepoAccess::SysrepoAccess()
-    : m_connection(std::make_shared<sysrepo::Connection>())
-    , m_session(std::make_shared<sysrepo::Session>(m_connection))
-    , m_schema(std::make_shared<YangSchema>(m_session->get_context()))
+    : m_connection()
+    , m_session(m_connection.sessionStart())
+    , m_schema(std::make_shared<YangSchema>(m_session.getContext()))
 {
-    try {
-        m_session = std::make_shared<sysrepo::Session>(m_connection);
-    } catch (sysrepo::sysrepo_exception& ex) {
-        reportErrors();
-    }
 }
 
 namespace {
@@ -94,11 +42,11 @@
 {
     switch (target) {
     case DatastoreTarget::Operational:
-        return SR_DS_OPERATIONAL;
+        return sysrepo::Datastore::Operational;
     case DatastoreTarget::Running:
-        return SR_DS_RUNNING;
+        return sysrepo::Datastore::Running;
     case DatastoreTarget::Startup:
-        return SR_DS_STARTUP;
+        return sysrepo::Datastore::Startup;
     }
 
     __builtin_unreachable();
@@ -110,9 +58,9 @@
     case DatastoreTarget::Operational:
     case DatastoreTarget::Running:
         // TODO: Doing candidate here doesn't work, why?
-        return SR_DS_RUNNING;
+        return sysrepo::Datastore::Running;
     case DatastoreTarget::Startup:
-        return SR_DS_STARTUP;
+        return sysrepo::Datastore::Startup;
     }
 
     __builtin_unreachable();
@@ -125,12 +73,12 @@
     Tree res;
 
     try {
-        m_session->session_switch_ds(targetToDs_get(m_target));
-        auto config = m_session->get_data(((path == "/") ? "/*" : path).c_str());
+        m_session.switchDatastore(targetToDs_get(m_target));
+        auto config = m_session.getData(((path == "/") ? "/*" : path).c_str());
         if (config) {
-            lyNodesToTree(res, config->tree_for());
+            lyNodesToTree(res, config->siblings());
         }
-    } catch (sysrepo::sysrepo_exception& ex) {
+    } catch (sysrepo::Error& ex) {
         reportErrors();
     }
     return res;
@@ -139,9 +87,10 @@
 void SysrepoAccess::setLeaf(const std::string& path, leaf_data_ value)
 {
     try {
-        m_session->session_switch_ds(targetToDs_set(m_target));
-        m_session->set_item(path.c_str(), boost::apply_visitor(valFromValue(), value), SR_EDIT_ISOLATE);
-    } catch (sysrepo::sysrepo_exception& ex) {
+        m_session.switchDatastore(targetToDs_set(m_target));
+        auto lyValue = value.type() == typeid(empty_) ? "" : leafDataToString(value);
+        m_session.setItem(path.c_str(), lyValue.c_str(), sysrepo::EditOptions::Isolate);
+    } catch (sysrepo::Error& ex) {
         reportErrors();
     }
 }
@@ -149,9 +98,9 @@
 void SysrepoAccess::createItem(const std::string& path)
 {
     try {
-        m_session->session_switch_ds(targetToDs_set(m_target));
-        m_session->set_item(path.c_str());
-    } catch (sysrepo::sysrepo_exception& ex) {
+        m_session.switchDatastore(targetToDs_set(m_target));
+        m_session.setItem(path.c_str(), nullptr);
+    } catch (sysrepo::Error& ex) {
         reportErrors();
     }
 }
@@ -159,27 +108,28 @@
 void SysrepoAccess::deleteItem(const std::string& path)
 {
     try {
-        // Have to use SR_EDIT_ISOLATE, because deleting something that's been set without committing is not supported
+        // Have to use sysrepo::EditOptions::Isolate, because deleting something that's been set without committing is
+        // not supported.
         // https://github.com/sysrepo/sysrepo/issues/1967#issuecomment-625085090
-        m_session->session_switch_ds(targetToDs_set(m_target));
-        m_session->delete_item(path.c_str(), SR_EDIT_ISOLATE);
-    } catch (sysrepo::sysrepo_exception& ex) {
+        m_session.switchDatastore(targetToDs_set(m_target));
+        m_session.deleteItem(path.c_str(), sysrepo::EditOptions::Isolate);
+    } catch (sysrepo::Error& ex) {
         reportErrors();
     }
 }
 
 struct impl_toSrMoveOp {
-    sr_move_position_t operator()(yang::move::Absolute& absolute)
+    sysrepo::MovePosition operator()(yang::move::Absolute& absolute)
     {
-        return absolute == yang::move::Absolute::Begin ? SR_MOVE_FIRST : SR_MOVE_LAST;
+        return absolute == yang::move::Absolute::Begin ? sysrepo::MovePosition::First : sysrepo::MovePosition::Last;
     }
-    sr_move_position_t operator()(yang::move::Relative& relative)
+    sysrepo::MovePosition operator()(yang::move::Relative& relative)
     {
-        return relative.m_position == yang::move::Relative::Position::After ? SR_MOVE_AFTER : SR_MOVE_BEFORE;
+        return relative.m_position == yang::move::Relative::Position::After ? sysrepo::MovePosition::After : sysrepo::MovePosition::Before;
     }
 };
 
-sr_move_position_t toSrMoveOp(std::variant<yang::move::Absolute, yang::move::Relative> move)
+sysrepo::MovePosition toSrMoveOp(std::variant<yang::move::Absolute, yang::move::Relative> move)
 {
     return std::visit(impl_toSrMoveOp{}, move);
 }
@@ -195,16 +145,16 @@
             destination = instanceToString(relative.m_path);
         }
     }
-    m_session->session_switch_ds(targetToDs_set(m_target));
-    m_session->move_item(source.c_str(), toSrMoveOp(move), destination.c_str(), destination.c_str());
+    m_session.switchDatastore(targetToDs_set(m_target));
+    m_session.moveItem(source.c_str(), toSrMoveOp(move), destination.c_str());
 }
 
 void SysrepoAccess::commitChanges()
 {
     try {
-        m_session->session_switch_ds(targetToDs_set(m_target));
-        m_session->apply_changes(OPERATION_TIMEOUT_MS, 1);
-    } catch (sysrepo::sysrepo_exception& ex) {
+        m_session.switchDatastore(targetToDs_set(m_target));
+        m_session.applyChanges(OPERATION_TIMEOUT_MS);
+    } catch (sysrepo::Error& ex) {
         reportErrors();
     }
 }
@@ -212,25 +162,25 @@
 void SysrepoAccess::discardChanges()
 {
     try {
-        m_session->session_switch_ds(targetToDs_set(m_target));
-        m_session->discard_changes();
-    } catch (sysrepo::sysrepo_exception& ex) {
+        m_session.switchDatastore(targetToDs_set(m_target));
+        m_session.discardChanges();
+    } catch (sysrepo::Error& ex) {
         reportErrors();
     }
 }
 
 DatastoreAccess::Tree SysrepoAccess::execute(const std::string& path, const Tree& input)
 {
-    auto inputNode = treeToRpcInput(m_session->get_context(), path, input);
-    m_session->session_switch_ds(targetToDs_set(m_target));
-    auto output = m_session->rpc_send(inputNode);
-    return rpcOutputToTree(path, output);
+    auto inputNode = treeToRpcInput(m_session.getContext(), path, input);
+    m_session.switchDatastore(targetToDs_set(m_target));
+    auto output = m_session.sendRPC(inputNode);
+    return rpcOutputToTree(output);
 }
 
 void SysrepoAccess::copyConfig(const Datastore source, const Datastore destination)
 {
-    m_session->session_switch_ds(toSrDatastore(destination));
-    m_session->copy_config(toSrDatastore(source), nullptr, OPERATION_TIMEOUT_MS, 1);
+    m_session.switchDatastore(toSrDatastore(destination));
+    m_session.copyConfig(toSrDatastore(source), nullptr, OPERATION_TIMEOUT_MS);
 }
 
 std::shared_ptr<Schema> SysrepoAccess::schema()
@@ -240,16 +190,14 @@
 
 [[noreturn]] void SysrepoAccess::reportErrors() const
 {
-    // I only use get_error to get error info, since the error code from
-    // sysrepo_exception doesn't really give any meaningful information. For
-    // example an "invalid argument" error could mean a node isn't enabled, or
-    // it could mean something totally different and there is no documentation
-    // for that, so it's better to just use the message sysrepo gives me.
-    auto srErrors = m_session->get_error();
     std::vector<DatastoreError> res;
 
-    for (size_t i = 0; i < srErrors->error_cnt(); i++) {
-        res.emplace_back(srErrors->message(i), srErrors->xpath(i) ? std::optional<std::string>{srErrors->xpath(i)} : std::nullopt);
+    for (const auto& err : m_session.getErrors()) {
+        res.emplace_back(err.errorMessage);
+    }
+
+    for (const auto& err : m_session.getNetconfErrors()) {
+        res.emplace_back(err.message, err.path);
     }
 
     throw DatastoreException(res);
@@ -258,47 +206,23 @@
 std::vector<ListInstance> SysrepoAccess::listInstances(const std::string& path)
 {
     std::vector<ListInstance> res;
-    auto lists = getItems(path);
+    auto lists = m_session.getData(path.c_str());
+    if (!lists) {
+        return res;
+    }
 
-    decltype(lists) instances;
-    auto wantedTree = *(m_schema->dataNodeFromPath(path)->find_path(path.c_str())->data().begin());
-    std::copy_if(lists.begin(), lists.end(), std::inserter(instances, instances.end()), [this, pathToCheck = wantedTree->schema()->path()](const auto& item) {
-        // This filters out non-instances.
-        if (item.second.type() != typeid(special_) || boost::get<special_>(item.second).m_value != SpecialValue::List) {
-            return false;
-        }
-
-        // Now, getItems is recursive: it gives everything including nested lists. So I try create a tree from the instance...
-        auto instanceTree = *(m_schema->dataNodeFromPath(item.first)->find_path(item.first.c_str())->data().begin());
-        // And then check if its schema path matches the list we actually want. This filters out lists which are not the ones I requested.
-        return instanceTree->schema()->path() == pathToCheck;
-    });
-
-    // If there are no instances, then just return
+    auto instances = lists->findXPath(path.c_str());
     if (instances.empty()) {
         return res;
     }
 
-    // I need to find out which keys does the list have. To do that, I create a
-    // tree from the first instance. This is gives me some top level node,
-    // which will be our list in case out list is a top-level node. In case it
-    // isn't, we have call find_path on the top level node. After that, I just
-    // retrieve the keys.
-    auto topLevelTree = m_schema->dataNodeFromPath(instances.begin()->first);
-    auto list = *(topLevelTree->find_path(path.c_str())->data().begin());
-    auto keys = libyang::Schema_Node_List{list->schema()}.keys();
+    auto keys = instances.front().schema().asList().keys();
 
-    // Creating a full tree at the same time from the values sysrepo gives me
-    // would be a pain (and after sysrepo switches to libyang meaningless), so
-    // I just use this algorithm to create data nodes one by one and get the
-    // key values from them.
     for (const auto& instance : instances) {
-        auto wantedList = *(m_schema->dataNodeFromPath(instance.first)->find_path(path.c_str())->data().begin());
         ListInstance instanceRes;
         for (const auto& key : keys) {
-            auto vec = wantedList->find_path(key->name())->data();
-            auto leaf = std::make_shared<libyang::Data_Node_Leaf_List>(*(vec.begin()));
-            instanceRes.emplace(key->name(), leafValueFromNode(leaf));
+            auto leaf = instance.findPath(key.name().data());
+            instanceRes.emplace(std::string{leaf->schema().name()}, leafValueFromNode(leaf->asTerm()));
         }
         res.emplace_back(instanceRes);
     }
@@ -308,6 +232,11 @@
 
 std::string SysrepoAccess::dump(const DataFormat format) const
 {
-    auto root = m_session->get_data("/*");
-    return root->print_mem(format == DataFormat::Xml ? LYD_XML : LYD_JSON, LYP_WITHSIBLINGS | LYP_FORMAT);
+    auto root = m_session.getData("/*");
+    auto str = root->printStr(format == DataFormat::Xml ? libyang::DataFormat::XML : libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings);
+    if (!str) {
+        return "";
+    }
+
+    return std::string{*str};
 }
diff --git a/src/sysrepo_access.hpp b/src/sysrepo_access.hpp
index 7a02d8b..0f736d1 100644
--- a/src/sysrepo_access.hpp
+++ b/src/sysrepo_access.hpp
@@ -9,6 +9,7 @@
 #pragma once
 
 #include <string>
+#include <sysrepo-cpp/Connection.hpp>
 #include "datastore_access.hpp"
 
 /*! \class DatastoreAccess
@@ -47,7 +48,7 @@
     std::vector<ListInstance> listInstances(const std::string& path) override;
     [[noreturn]] void reportErrors() const;
 
-    std::shared_ptr<sysrepo::Connection> m_connection;
-    std::shared_ptr<sysrepo::Session> m_session;
+    sysrepo::Connection m_connection;
+    sysrepo::Session m_session;
     std::shared_ptr<YangSchema> m_schema;
 };
diff --git a/src/yang_access.cpp b/src/yang_access.cpp
index 0555616..72e38e7 100644
--- a/src/yang_access.cpp
+++ b/src/yang_access.cpp
@@ -2,8 +2,8 @@
 #include <experimental/iterator>
 #include <fstream>
 #include <iostream>
-#include <libyang/Tree_Data.hpp>
-#include <libyang/libyang.h>
+#include <libyang-cpp/DataNode.hpp>
+#include <libyang-cpp/Utils.hpp>
 #include "UniqueResource.hpp"
 #include "libyang_utils.hpp"
 #include "utils.hpp"
@@ -11,35 +11,21 @@
 #include "yang_schema.hpp"
 
 namespace {
-template <typename Type> using lyPtrDeleter_type = void (*)(Type*);
-template <typename Type> const lyPtrDeleter_type<Type> lyPtrDeleter;
-template <> const auto lyPtrDeleter<ly_set> = ly_set_free;
-template <> const auto lyPtrDeleter<ly_ctx> = static_cast<lyPtrDeleter_type<ly_ctx>>([] (auto* ptr) {ly_ctx_destroy(ptr, nullptr);});
-template <> const auto lyPtrDeleter<lyd_node> = lyd_free_withsiblings;
-
-template <typename Type>
-auto lyWrap(Type* ptr)
-{
-    return std::unique_ptr<Type, lyPtrDeleter_type<Type>>{ptr, lyPtrDeleter<Type>};
-}
-
 // Convenient for functions that take m_datastore as an argument
-using DatastoreType = std::unique_ptr<lyd_node, lyPtrDeleter_type<lyd_node>>;
+using DatastoreType = std::optional<libyang::DataNode>;
 }
 
 YangAccess::YangAccess()
-    : m_ctx(lyWrap(ly_ctx_new(nullptr, LY_CTX_DISABLE_SEARCHDIR_CWD)))
-    , m_datastore(lyWrap<lyd_node>(nullptr))
-    , m_schema(std::make_shared<YangSchema>(libyang::create_new_Context(m_ctx.get())))
-    , m_validation_mode(LYD_OPT_DATA)
+    : m_ctx(nullptr, libyang::ContextOptions::DisableSearchCwd)
+    , m_datastore(std::nullopt)
+    , m_schema(std::make_shared<YangSchema>(m_ctx))
 {
 }
 
 YangAccess::YangAccess(std::shared_ptr<YangSchema> schema)
-    : m_ctx(schema->m_context->swig_ctx(), [](auto) {})
-    , m_datastore(lyWrap<lyd_node>(nullptr))
+    : m_ctx(schema->m_context)
+    , m_datastore(std::nullopt)
     , m_schema(schema)
-    , m_validation_mode(LYD_OPT_RPC)
 {
 }
 
@@ -47,78 +33,69 @@
 
 [[noreturn]] void YangAccess::getErrorsAndThrow() const
 {
-    auto errors = libyang::get_ly_errors(libyang::create_new_Context(m_ctx.get()));
     std::vector<DatastoreError> errorsRes;
-    for (const auto& error : errors) {
-        using namespace std::string_view_literals;
-        errorsRes.emplace_back(error->errmsg(), error->errpath() != ""sv ? std::optional{error->errpath()} : std::nullopt);
-    }
 
+    for (const auto& err : m_ctx.getErrors()) {
+        errorsRes.emplace_back(err.message, err.path);
+    }
     throw DatastoreException(errorsRes);
 }
 
 void YangAccess::impl_newPath(const std::string& path, const std::optional<std::string>& value)
 {
-    auto newNode = lyd_new_path(m_datastore.get(), m_ctx.get(), path.c_str(), value ? (void*)value->c_str() : nullptr, LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_UPDATE);
-    if (!newNode) {
+    try {
+        if (m_datastore) {
+            m_datastore->newPath(path.c_str(), value ? value->c_str() : nullptr, libyang::CreationOptions::Update);
+        } else {
+            m_datastore = m_ctx.newPath(path.c_str(), value ? value->c_str() : nullptr, libyang::CreationOptions::Update);
+        }
+    } catch (libyang::Error&) {
         getErrorsAndThrow();
     }
-    if (!m_datastore) {
-        m_datastore = lyWrap(newNode);
-    }
 }
 
 namespace {
-void impl_unlink(DatastoreType& datastore, lyd_node* what)
+void impl_unlink(DatastoreType& datastore, libyang::DataNode what)
 {
     // If the node to be unlinked is the one our datastore variable points to, we need to find a new one to point to (one of its siblings)
-    if (datastore.get() == what) {
-        auto oldDatastore = datastore.release();
-        if (oldDatastore->prev != oldDatastore) {
-            datastore = lyWrap(oldDatastore->prev);
-        } else {
-            datastore = lyWrap(oldDatastore->next);
-        }
+
+    if (datastore == what) {
+        auto oldDatastore = datastore;
+        do {
+            datastore = datastore->previousSibling();
+            if (datastore == oldDatastore) {
+                // We have gone all the way back to our original node, which means it's the only node in our
+                // datastore.
+                datastore = std::nullopt;
+                break;
+            }
+        } while (datastore->schema().module().name() == "ietf-yang-library");
     }
 
-    lyd_unlink(what);
+    what.unlink();
 }
 }
 
 void YangAccess::impl_removeNode(const std::string& path)
 {
-    auto set = lyWrap(lyd_find_path(m_datastore.get(), path.c_str()));
-    if (!set || set->number == 0) {
-        // Check if schema node exists - lyd_find_path first checks if the first argument is non-null before checking for path validity
-        if (!ly_ctx_get_node(m_ctx.get(), nullptr, path.c_str(), 0)) {
-            throw DatastoreException{{DatastoreError{"Schema node doesn't exist.", path}}};
-        }
-        // Check if libyang found another error
-        if (ly_err_first(m_ctx.get())) {
-            getErrorsAndThrow();
-        }
-
+    if (!m_datastore) {
+        // Otherwise the datastore just doesn't contain the wanted node.
+        throw DatastoreException{{DatastoreError{"Datastore is empty.", path}}};
+    }
+    auto toRemove = m_datastore->findPath(path.c_str());
+    if (!toRemove) {
         // Otherwise the datastore just doesn't contain the wanted node.
         throw DatastoreException{{DatastoreError{"Data node doesn't exist.", path}}};
     }
 
-    auto toRemove = set->set.d[0];
-
-    impl_unlink(m_datastore, toRemove);
-
-    lyd_free(toRemove);
+    impl_unlink(m_datastore, *toRemove);
 }
 
 void YangAccess::validate()
 {
-    auto datastore = m_datastore.release();
-
-    if (m_validation_mode == LYD_OPT_RPC) {
-        lyd_validate(&datastore, m_validation_mode, nullptr);
-    } else {
-        lyd_validate(&datastore, m_validation_mode | LYD_OPT_DATA_NO_YANGLIB, m_ctx.get());
+    if (m_datastore) {
+        libyang::validateAll(m_datastore);
     }
-    m_datastore = lyWrap(datastore);
 }
 
 DatastoreAccess::Tree YangAccess::getItems(const std::string& path) const
@@ -128,10 +105,9 @@
         return res;
     }
 
-    auto set = lyWrap(lyd_find_path(m_datastore.get(), path == "/" ? "/*" : path.c_str()));
-    auto setWrapper = libyang::Set(set.get(), nullptr);
-    std::optional<std::string> ignoredXPathPrefix;
-    lyNodesToTree(res, setWrapper.data());
+    auto set = m_datastore->findXPath(path == "/" ? "/*" : path.c_str());
+
+    lyNodesToTree(res, set);
     return res;
 }
 
@@ -154,66 +130,60 @@
 namespace {
 struct impl_moveItem {
     DatastoreType& m_datastore;
-    lyd_node* m_sourceNode;
+    libyang::DataNode m_sourceNode;
 
     void operator()(yang::move::Absolute absolute) const
     {
-        auto set = lyWrap(lyd_find_instance(m_sourceNode, m_sourceNode->schema));
-        if (set->number == 1) { // m_sourceNode is the sole instance, do nothing
+        auto set = m_sourceNode.findXPath(m_sourceNode.schema().path().get().get());
+        if (set.size() == 1) { // m_sourceNode is the sole instance, do nothing
             return;
         }
 
-        doUnlink();
         switch (absolute) {
         case yang::move::Absolute::Begin:
-            if (set->set.d[0] == m_sourceNode) { // List is already at the beginning, do nothing
+            if (set.front() == m_sourceNode) { // List is already at the beginning, do nothing
                 return;
             }
-            lyd_insert_before(set->set.d[0], m_sourceNode);
-            return;
+            set.front().insertBefore(m_sourceNode);
+            break;
         case yang::move::Absolute::End:
-            if (set->set.d[set->number - 1] == m_sourceNode) { // List is already at the end, do nothing
+            if (set.back() == m_sourceNode) { // List is already at the end, do nothing
                 return;
             }
-            lyd_insert_after(set->set.d[set->number - 1], m_sourceNode);
-            return;
+            set.back().insertAfter(m_sourceNode);
+            break;
         }
+        m_datastore = m_datastore->firstSibling();
     }
 
     void operator()(const yang::move::Relative& relative) const
     {
-        auto keySuffix = m_sourceNode->schema->nodetype == LYS_LIST ? instanceToString(relative.m_path)
+        auto keySuffix = m_sourceNode.schema().nodeType() == libyang::NodeType::List ? instanceToString(relative.m_path)
                                                                     : leafDataToString(relative.m_path.at("."));
-        lyd_node* destNode;
-        lyd_find_sibling_val(m_sourceNode, m_sourceNode->schema, keySuffix.c_str(), &destNode);
+        auto destNode = m_sourceNode.findSiblingVal(m_sourceNode.schema(), keySuffix.c_str());
 
-        doUnlink();
         if (relative.m_position == yang::move::Relative::Position::After) {
-            lyd_insert_after(destNode, m_sourceNode);
+            destNode->insertAfter(m_sourceNode);
         } else {
-            lyd_insert_before(destNode, m_sourceNode);
+            destNode->insertBefore(m_sourceNode);
         }
     }
-
-private:
-    void doUnlink() const
-    {
-        impl_unlink(m_datastore, m_sourceNode);
-    }
 };
 }
 
 void YangAccess::moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move)
 {
-    auto set = lyWrap(lyd_find_path(m_datastore.get(), source.c_str()));
-    if (!set) { // Error, the node probably doesn't exist in the schema
-        getErrorsAndThrow();
+    if (!m_datastore) {
+        throw DatastoreException{{DatastoreError{"Datastore is empty.", source}}};
     }
-    if (set->number == 0) {
-        return;
+
+    auto sourceNode = m_datastore->findPath(source.c_str());
+
+    if (!sourceNode) {
+        // The datastore doesn't contain the wanted node.
+        throw DatastoreException{{DatastoreError{"Data node doesn't exist.", source}}};
     }
-    auto sourceNode = set->set.d[0];
-    std::visit(impl_moveItem{m_datastore, sourceNode}, move);
+    std::visit(impl_moveItem{m_datastore, *sourceNode}, move);
 }
 
 void YangAccess::commitChanges()
@@ -227,16 +197,24 @@
 
 [[noreturn]] DatastoreAccess::Tree YangAccess::execute(const std::string& path, const Tree& input)
 {
-    auto root = lyWrap(lyd_new_path(nullptr, m_ctx.get(), path.c_str(), nullptr, LYD_ANYDATA_CONSTSTRING, 0));
-    if (!root) {
-        getErrorsAndThrow();
-    }
+    auto root = [&path, this]  {
+        try {
+            return m_ctx.newPath(path.c_str());
+        } catch (libyang::ErrorWithCode& err) {
+            getErrorsAndThrow();
+        }
+    }();
+
     for (const auto& [k, v] : input) {
         if (v.type() == typeid(special_) && boost::get<special_>(v).m_value != SpecialValue::PresenceContainer) {
             continue;
         }
 
-        lyd_new_path(root.get(), m_ctx.get(), k.c_str(), (void*)leafDataToString(v).c_str(), LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_UPDATE);
+        try {
+            root.newPath(k.c_str(), leafDataToString(v).c_str(), libyang::CreationOptions::Update);
+        } catch (libyang::ErrorWithCode& err) {
+            getErrorsAndThrow();
+        }
     }
     throw std::logic_error("in-memory datastore doesn't support executing RPC/action");
 }
@@ -244,7 +222,7 @@
 void YangAccess::copyConfig(const Datastore source, const Datastore dest)
 {
     if (source == Datastore::Startup && dest == Datastore::Running) {
-        m_datastore = nullptr;
+        m_datastore = std::nullopt;
     }
 }
 
@@ -260,16 +238,14 @@
         return res;
     }
 
-    auto instances = lyWrap(lyd_find_path(m_datastore.get(), path.c_str()));
-    auto instancesWrapper = libyang::Set(instances.get(), nullptr);
-    for (const auto& list : instancesWrapper.data()) {
+    auto instances = m_datastore->findXPath(path.c_str());
+    for (const auto& list : instances) {
         ListInstance instance;
-        for (const auto& child : list->child()->tree_for()) {
-            if (child->schema()->nodetype() == LYS_LEAF) {
-                libyang::Schema_Node_Leaf leafSchema(child->schema());
-                if (leafSchema.is_key()) {
-                    auto leafData = std::make_shared<libyang::Data_Node_Leaf_List>(child);
-                    instance.insert({leafSchema.name(), leafValueFromNode(leafData)});
+        for (const auto& child : list.child()->siblings()) {
+            if (child.schema().nodeType() == libyang::NodeType::Leaf) {
+                auto leafSchema(child.schema().asLeaf());
+                if (leafSchema.isKey()) {
+                    instance.insert({std::string{leafSchema.name()}, leafValueFromNode(child.asTerm())});
                 }
             }
         }
@@ -280,16 +256,16 @@
 
 std::string YangAccess::dump(const DataFormat format) const
 {
-    char* output;
-    lyd_print_mem(&output, m_datastore.get(), format == DataFormat::Xml ? LYD_XML : LYD_JSON, LYP_WITHSIBLINGS | LYP_FORMAT);
-    std::unique_ptr<char, decltype(&free)> deleter{output, free};
-
-    if (output) {
-        std::string res = output;
-        return res;
+    if (!m_datastore) {
+        return "";
     }
 
-    return "";
+    auto str = m_datastore->firstSibling().printStr(format == DataFormat::Xml ? libyang::DataFormat::XML : libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings);
+    if (!str) {
+        return "";
+    }
+
+    return std::string{*str};
 }
 
 void YangAccess::loadModule(const std::string& name)
@@ -307,9 +283,9 @@
     m_schema->addSchemaDirectory(path.c_str());
 }
 
-void YangAccess::enableFeature(const std::string& module, const std::string& feature)
+void YangAccess::setEnabledFeatures(const std::string& module, const std::vector<std::string>& features)
 {
-    m_schema->enableFeature(module, feature);
+    m_schema->setEnabledFeatures(module, features);
 }
 
 void YangAccess::addDataFile(const std::string& path, const StrictDataParsing strict)
@@ -320,20 +296,16 @@
 
     std::cout << "Parsing \"" << path << "\" as " << (firstChar == '{' ? "JSON" : "XML") << "...\n";
 
-    auto parseFlags = LYD_OPT_DATA | LYD_OPT_DATA_NO_YANGLIB | LYD_OPT_TRUSTED;
-    if (strict == StrictDataParsing::Yes) {
-        parseFlags |= LYD_OPT_STRICT;
-    }
-    auto dataNode = lyd_parse_path(m_ctx.get(), path.c_str(), firstChar == '{' ? LYD_JSON : LYD_XML, parseFlags);
-
-    if (!dataNode) {
-        throw std::runtime_error("Supplied data file " + path + " couldn't be parsed.");
-    }
+    auto dataNode = m_ctx.parseDataPath(
+            path.c_str(),
+            firstChar == '{' ? libyang::DataFormat::JSON : libyang::DataFormat::XML,
+            strict == StrictDataParsing::Yes ? std::optional{libyang::ParseOptions::Strict} : std::nullopt,
+            libyang::ValidationOptions::Present);
 
     if (!m_datastore) {
-        m_datastore = lyWrap(dataNode);
+        m_datastore = dataNode;
     } else {
-        lyd_merge(m_datastore.get(), dataNode, LYD_OPT_DESTRUCT);
+        m_datastore->merge(*dataNode);
     }
 
     validate();
diff --git a/src/yang_access.hpp b/src/yang_access.hpp
index 64538f0..aab5d81 100644
--- a/src/yang_access.hpp
+++ b/src/yang_access.hpp
@@ -7,6 +7,7 @@
 
 #pragma once
 
+#include <libyang-cpp/Context.hpp>
 #include "datastore_access.hpp"
 
 /*! \class YangAccess
@@ -40,7 +41,7 @@
 
     std::shared_ptr<Schema> schema() override;
 
-    void enableFeature(const std::string& module, const std::string& feature);
+    void setEnabledFeatures(const std::string& module, const std::vector<std::string>& features);
     [[nodiscard]] std::string dump(const DataFormat format) const override;
 
     void loadModule(const std::string& name);
@@ -57,8 +58,7 @@
     void impl_removeNode(const std::string& path);
     void validate();
 
-    std::unique_ptr<ly_ctx, void (*)(ly_ctx*)> m_ctx;
-    std::unique_ptr<lyd_node, void (*)(lyd_node*)> m_datastore;
+    libyang::Context m_ctx;
+    std::optional<libyang::DataNode> m_datastore;
     std::shared_ptr<YangSchema> m_schema;
-    const int m_validation_mode;
 };
diff --git a/src/yang_schema.cpp b/src/yang_schema.cpp
index b964d31..f96aa46 100644
--- a/src/yang_schema.cpp
+++ b/src/yang_schema.cpp
@@ -6,9 +6,8 @@
  *
 */
 
-#include <libyang/Libyang.hpp>
-#include <libyang/Tree_Data.hpp>
-#include <libyang/Tree_Schema.hpp>
+#include <libyang-cpp/Enum.hpp>
+#include <libyang-cpp/Utils.hpp>
 #include <string_view>
 #include "UniqueResource.hpp"
 #include "utils.hpp"
@@ -33,11 +32,11 @@
 };
 
 YangSchema::YangSchema()
-    : m_context(std::make_shared<libyang::Context>(nullptr, LY_CTX_DISABLE_SEARCHDIR_CWD))
+    : m_context(nullptr, libyang::ContextOptions::DisableSearchDirs | libyang::ContextOptions::SetPrivParsed)
 {
 }
 
-YangSchema::YangSchema(std::shared_ptr<libyang::Context> lyCtx)
+YangSchema::YangSchema(libyang::Context lyCtx)
     : m_context(lyCtx)
 {
 }
@@ -46,29 +45,22 @@
 
 void YangSchema::addSchemaString(const char* schema)
 {
-    if (!m_context->parse_module_mem(schema, LYS_IN_YANG)) {
-        throw YangLoadError("Couldn't load schema");
-    }
+    m_context.parseModuleMem(schema, libyang::SchemaFormat::YANG);
 }
 
 void YangSchema::addSchemaDirectory(const char* directoryName)
 {
-    if (m_context->set_searchdir(directoryName)) {
-        throw YangLoadError("Couldn't add schema search directory");
-    }
+    m_context.setSearchDir(directoryName);
 }
 
 void YangSchema::addSchemaFile(const char* filename)
 {
-    if (!m_context->parse_module_path(filename, LYS_IN_YANG)) {
-        throw YangLoadError("Couldn't load schema");
-    }
+    m_context.parseModulePath(filename, libyang::SchemaFormat::YANG);
 }
 
 bool YangSchema::isModule(const std::string& name) const
 {
-    const auto set = modules();
-    return set.find(name) != set.end();
+    return m_context.getModuleImplemented(name.c_str()).has_value();
 }
 
 bool YangSchema::listHasKey(const schemaPath_& listPath, const std::string& key) const
@@ -80,49 +72,52 @@
 bool YangSchema::leafIsKey(const std::string& leafPath) const
 {
     auto node = getSchemaNode(leafPath);
-    if (!node || node->nodetype() != LYS_LEAF) {
+    if (!node || node->nodeType() != libyang::NodeType::Leaf) {
         return false;
     }
 
-    return libyang::Schema_Node_Leaf{node}.is_key().get();
+    return node->asLeaf().isKey();
 }
 
-libyang::S_Schema_Node YangSchema::impl_getSchemaNode(const std::string& node) const
+std::optional<libyang::SchemaNode> YangSchema::impl_getSchemaNode(const std::string& node) const
 {
-    // If no node is found find_path prints an error message, so we have to
-    // disable logging
-    // https://github.com/CESNET/libyang/issues/753
-    {
-        int oldOptions;
-        auto logBlocker = make_unique_resource(
-            [&oldOptions]() {
-                oldOptions = libyang::set_log_options(0);
-            },
-            [&oldOptions]() {
-                libyang::set_log_options(oldOptions);
-            });
-        auto res = m_context->get_node(nullptr, node.c_str());
-        if (!res) { // If no node is found, try output rpc nodes too.
-            res = m_context->get_node(nullptr, node.c_str(), 1);
+    // libyang::Context::findPath throws an exception, when no matching schema node is found. This exception has the
+    // ValidationFailure error code. We will catch that exception (and rethrow if it's not the correct error code.
+    //
+    // Also, we need to use findPath twice if we're trying to find output nodes.
+    try {
+        return m_context.findPath(node.c_str());
+    } catch (libyang::ErrorWithCode& err) {
+        if (err.code() != libyang::ErrorCode::ValidationFailure) {
+            throw;
         }
-        return res;
     }
+    try {
+        return m_context.findPath(node.c_str(), libyang::OutputNodes::Yes);
+    } catch (libyang::ErrorWithCode& err) {
+        if (err.code() != libyang::ErrorCode::ValidationFailure) {
+            throw;
+        }
+    }
+
+    // We didn't find a matching node.
+    return std::nullopt;
 }
 
 
-libyang::S_Schema_Node YangSchema::getSchemaNode(const std::string& node) const
+std::optional<libyang::SchemaNode> YangSchema::getSchemaNode(const std::string& node) const
 {
     return impl_getSchemaNode(node);
 }
 
-libyang::S_Schema_Node YangSchema::getSchemaNode(const schemaPath_& location, const ModuleNodePair& node) const
+std::optional<libyang::SchemaNode> YangSchema::getSchemaNode(const schemaPath_& location, const ModuleNodePair& node) const
 {
     std::string absPath = joinPaths(pathToSchemaString(location, Prefixes::Always), fullNodeName(location, node));
 
     return impl_getSchemaNode(absPath);
 }
 
-libyang::S_Schema_Node YangSchema::getSchemaNode(const schemaPath_& listPath) const
+std::optional<libyang::SchemaNode> YangSchema::getSchemaNode(const schemaPath_& listPath) const
 {
     std::string absPath = pathToSchemaString(listPath, Prefixes::Always);
     return impl_getSchemaNode(absPath);
@@ -131,174 +126,121 @@
 const std::set<std::string> YangSchema::listKeys(const schemaPath_& listPath) const
 {
     auto node = getSchemaNode(listPath);
-    if (node->nodetype() != LYS_LIST) {
+    if (node->nodeType() != libyang::NodeType::List) {
         return {};
     }
 
-    auto list = std::make_shared<libyang::Schema_Node_List>(node);
     std::set<std::string> keys;
-    const auto& keysVec = list->keys();
+    auto keysVec = node->asList().keys();
 
-    std::transform(keysVec.begin(), keysVec.end(), std::inserter(keys, keys.begin()), [](const auto& it) { return it->name(); });
+    std::transform(keysVec.begin(), keysVec.end(), std::inserter(keys, keys.begin()), [](const auto& it) { return std::string{it.name()}; });
     return keys;
 }
 
-namespace {
-enum class ResolveMode {
-    Enum,
-    Identity
-};
-/** @brief Resolves a typedef to a type which defines values.
- * When we need allowed values of a type and that type is a typedef, we need to recurse into the typedef until we find a
- * type which defines values. These values are the allowed values.
- * Example:
- *
- * typedef MyOtherEnum {
- *   type enumeration {
- *     enum "A";
- *     enum "B";
- *   }
- * }
- *
- * typedef MyEnum {
- *   type MyOtherEnum;
- * }
- *
- * If `toResolve` points to MyEnum, then just doing ->enums()->enm() returns nothing and that means that this particular
- * typedef (MyEnum) did not say which values are allowed. So, we need to dive into the parent enum (MyOtherEnum) with
- * ->der()->type(). This typedef (MyOtherEnum) DID specify allowed values and enums()->enm() WILL contain them. These
- *  values are the only relevant values and we don't care about other parent typedefs. We return these values to the
- *  caller.
- *
- *  For enums, this function simply returns all allowed enums.
- *  For identities, this function returns which bases `toResolve` has.
- */
-template <ResolveMode TYPE>
-auto resolveTypedef(const libyang::S_Type& toResolve)
+std::set<enum_> enumValues(const libyang::Type& type)
 {
-    auto type = toResolve;
-    auto getValuesFromType = [] (const libyang::S_Type& type) {
-        if constexpr (TYPE == ResolveMode::Identity) {
-            return type->info()->ident()->ref();
-        } else {
-            return type->info()->enums()->enm();
-        }
-    };
-    auto values = getValuesFromType(type);
-    while (values.empty()) {
-        type = type->der()->type();
-        values = getValuesFromType(type);
-    }
-
-    return values;
-}
-
-std::set<enum_> enumValues(const libyang::S_Type& type)
-{
-    auto values = resolveTypedef<ResolveMode::Enum>(type);
-
-    std::vector<libyang::S_Type_Enum> enabled;
-    std::copy_if(values.begin(), values.end(), std::back_inserter(enabled), [](const libyang::S_Type_Enum& it) {
-        auto iffeatures = it->iffeature();
-        return std::all_of(iffeatures.begin(), iffeatures.end(), [](auto it) { return it->value(); });
-    });
-
+    auto enums = type.asEnum().items();
     std::set<enum_> enumSet;
-    std::transform(enabled.begin(), enabled.end(), std::inserter(enumSet, enumSet.end()), [](auto it) { return enum_{it->name()}; });
+    std::transform(enums.begin(), enums.end(), std::inserter(enumSet, enumSet.end()), [](auto it) { return enum_{std::string{it.name}}; });
     return enumSet;
 }
 
-std::set<identityRef_> validIdentities(const libyang::S_Type& type)
+std::set<identityRef_> validIdentities(const libyang::Type& type)
 {
     std::set<identityRef_> identSet;
 
-    for (auto base : resolveTypedef<ResolveMode::Identity>(type)) { // Iterate over all bases
-        // Iterate over derived identities (this is recursive!)
-        if (auto der = base->der()) {
-            for (auto derived : der->schema()) {
-                identSet.emplace(derived->module()->name(), derived->name());
-            }
+    std::function<void(const std::vector<libyang::Identity>&)> impl = [&identSet, &impl] (const std::vector<libyang::Identity>& idents) {
+        if (idents.empty()) {
+            return;
         }
+
+        for (const auto& ident : idents) {
+            identSet.emplace(std::string{ident.module().name()}, std::string{ident.name()});
+            impl(ident.derived());
+        }
+    };
+
+    for (const auto& base : type.asIdentityRef().bases()) {
+        impl(base.derived());
     }
 
     return identSet;
 }
 
-std::string leafrefPath(const libyang::S_Type& type)
+std::string leafrefPath(const libyang::Type& type)
 {
-    return type->info()->lref()->target()->path(LYS_PATH_FIRST_PREFIX);
-}
+    return std::string{type.asLeafRef().path()};
 }
 
 template <typename NodeType>
-yang::TypeInfo YangSchema::impl_leafType(const libyang::S_Schema_Node& node) const
+yang::TypeInfo YangSchema::impl_leafType(const NodeType& node) const
 {
     using namespace std::string_literals;
     auto leaf = std::make_shared<NodeType>(node);
     auto leafUnits = leaf->units();
-    std::function<yang::TypeInfo(std::shared_ptr<libyang::Type>)> resolveType;
-    resolveType = [this, &resolveType, leaf, leafUnits](std::shared_ptr<libyang::Type> type) -> yang::TypeInfo {
+    std::function<yang::TypeInfo(const libyang::Type&)> resolveType;
+    resolveType = [&resolveType, leaf, leafUnits](const libyang::Type& type) -> yang::TypeInfo {
         yang::LeafDataType resType;
-        switch (type->base()) {
-        case LY_TYPE_STRING:
+        switch (type.base()) {
+        case libyang::LeafBaseType::String:
             resType.emplace<yang::String>();
             break;
-        case LY_TYPE_DEC64:
+        case libyang::LeafBaseType::Dec64:
             resType.emplace<yang::Decimal>();
             break;
-        case LY_TYPE_BOOL:
+        case libyang::LeafBaseType::Bool:
             resType.emplace<yang::Bool>();
             break;
-        case LY_TYPE_INT8:
+        case libyang::LeafBaseType::Int8:
             resType.emplace<yang::Int8>();
             break;
-        case LY_TYPE_INT16:
+        case libyang::LeafBaseType::Int16:
             resType.emplace<yang::Int16>();
             break;
-        case LY_TYPE_INT32:
+        case libyang::LeafBaseType::Int32:
             resType.emplace<yang::Int32>();
             break;
-        case LY_TYPE_INT64:
+        case libyang::LeafBaseType::Int64:
             resType.emplace<yang::Int64>();
             break;
-        case LY_TYPE_UINT8:
+        case libyang::LeafBaseType::Uint8:
             resType.emplace<yang::Uint8>();
             break;
-        case LY_TYPE_UINT16:
+        case libyang::LeafBaseType::Uint16:
             resType.emplace<yang::Uint16>();
             break;
-        case LY_TYPE_UINT32:
+        case libyang::LeafBaseType::Uint32:
             resType.emplace<yang::Uint32>();
             break;
-        case LY_TYPE_UINT64:
+        case libyang::LeafBaseType::Uint64:
             resType.emplace<yang::Uint64>();
             break;
-        case LY_TYPE_BINARY:
+        case libyang::LeafBaseType::Binary:
             resType.emplace<yang::Binary>();
             break;
-        case LY_TYPE_EMPTY:
+        case libyang::LeafBaseType::Empty:
             resType.emplace<yang::Empty>();
             break;
-        case LY_TYPE_ENUM:
+        case libyang::LeafBaseType::Enum:
             resType.emplace<yang::Enum>(enumValues(type));
             break;
-        case LY_TYPE_IDENT:
+        case libyang::LeafBaseType::IdentityRef:
             resType.emplace<yang::IdentityRef>(validIdentities(type));
             break;
-        case LY_TYPE_LEAFREF:
-            resType.emplace<yang::LeafRef>(::leafrefPath(type), std::make_unique<yang::TypeInfo>(leafType(::leafrefPath(type))));
+        case libyang::LeafBaseType::Leafref:
+            resType.emplace<yang::LeafRef>(::leafrefPath(type), std::make_unique<yang::TypeInfo>(resolveType(type.asLeafRef().resolvedType())));
             break;
-        case LY_TYPE_BITS: {
+        case libyang::LeafBaseType::Bits: {
             auto resBits = yang::Bits{};
-            for (const auto& bit : type->info()->bits()->bit()) {
-                resBits.m_allowedValues.emplace(bit->name());
+            for (const auto& bit : type.asBits().items()) {
+                resBits.m_allowedValues.emplace(std::string{bit.name});
             }
             resType.emplace<yang::Bits>(std::move(resBits));
             break;
         }
-        case LY_TYPE_UNION: {
+        case libyang::LeafBaseType::Union: {
             auto resUnion = yang::Union{};
-            for (auto unionType : type->info()->uni()->types()) {
+            for (auto unionType : type.asUnion().types()) {
                 resUnion.m_unionTypes.emplace_back(resolveType(unionType));
             }
             resType.emplace<yang::Union>(std::move(resUnion));
@@ -306,48 +248,26 @@
         }
         default:
             using namespace std::string_literals;
-            throw UnsupportedYangTypeException("the type of "s + leaf->name() + " is not supported: " + std::to_string(leaf->type()->base()));
+            throw UnsupportedYangTypeException("the type of "s +
+                    std::string{leaf->name()} +
+                    " is not supported: " +
+                    std::to_string(std::underlying_type_t<libyang::LeafBaseType>(leaf->valueType().base())));
         }
 
-        std::optional<std::string> resUnits;
 
-        if (leafUnits) {
-            resUnits = leafUnits;
-        } else {
-            for (auto parentTypedef = type->der(); parentTypedef; parentTypedef = parentTypedef->type()->der()) {
-                auto units = parentTypedef->units();
-                if (units) {
-                    resUnits = units;
-                    break;
-                }
-            }
-        }
-
-        std::optional<std::string> resDescription;
-
-        // checking for parentTypedef->type()->der() means I'm going to enter inside base types like "string". These
-        // also have a description, but it isn't too helpful ("human-readable string")
-        for (auto parentTypedef = type->der(); parentTypedef && parentTypedef->type()->der(); parentTypedef = parentTypedef->type()->der()) {
-            auto dsc = parentTypedef->dsc();
-            if (dsc) {
-                resDescription = dsc;
-                break;
-            }
-        }
-
-        return yang::TypeInfo(resType, resUnits, resDescription);
+        return yang::TypeInfo(resType, std::optional<std::string>{leafUnits}, std::optional<std::string>{type.description()});
     };
-    return resolveType(leaf->type());
+    return resolveType(leaf->valueType());
 }
 
 yang::TypeInfo YangSchema::leafType(const schemaPath_& location, const ModuleNodePair& node) const
 {
     auto lyNode = getSchemaNode(location, node);
-    switch (lyNode->nodetype()) {
-    case LYS_LEAF:
-        return impl_leafType<libyang::Schema_Node_Leaf>(lyNode);
-    case LYS_LEAFLIST:
-        return impl_leafType<libyang::Schema_Node_Leaflist>(lyNode);
+    switch (lyNode->nodeType()) {
+    case libyang::NodeType::Leaf:
+        return impl_leafType(lyNode->asLeaf());
+    case libyang::NodeType::Leaflist:
+        return impl_leafType(lyNode->asLeafList());
     default:
         throw std::logic_error("YangSchema::leafType: type must be leaf or leaflist");
     }
@@ -356,11 +276,11 @@
 yang::TypeInfo YangSchema::leafType(const std::string& path) const
 {
     auto lyNode = getSchemaNode(path);
-    switch (lyNode->nodetype()) {
-    case LYS_LEAF:
-        return impl_leafType<libyang::Schema_Node_Leaf>(lyNode);
-    case LYS_LEAFLIST:
-        return impl_leafType<libyang::Schema_Node_Leaflist>(lyNode);
+    switch (lyNode->nodeType()) {
+    case libyang::NodeType::Leaf:
+        return impl_leafType(lyNode->asLeaf());
+    case libyang::NodeType::Leaflist:
+        return impl_leafType(lyNode->asLeafList());
     default:
         throw std::logic_error("YangSchema::leafType: type must be leaf or leaflist");
     }
@@ -368,23 +288,22 @@
 
 std::optional<std::string> YangSchema::leafTypeName(const std::string& path) const
 {
-    libyang::Schema_Node_Leaf leaf(getSchemaNode(path));
-    return leaf.type()->der().get() && leaf.type()->der()->type()->der().get() ? std::optional{leaf.type()->der()->name()} : std::nullopt;
+    auto leaf = getSchemaNode(path)->asLeaf();
+    return std::string{leaf.valueType().name()};
 }
 
 std::string YangSchema::leafrefPath(const std::string& leafrefPath) const
 {
     using namespace std::string_literals;
-    libyang::Schema_Node_Leaf leaf(getSchemaNode(leafrefPath));
-    return leaf.type()->info()->lref()->target()->path(LYS_PATH_FIRST_PREFIX);
+    return ::leafrefPath(getSchemaNode(leafrefPath)->asLeaf().valueType());
 }
 
 std::set<std::string> YangSchema::modules() const
 {
-    const auto& modules = m_context->get_module_iter();
+    const auto& modules = m_context.modules();
 
     std::set<std::string> res;
-    std::transform(modules.begin(), modules.end(), std::inserter(res, res.end()), [](const auto module) { return module->name(); });
+    std::transform(modules.begin(), modules.end(), std::inserter(res, res.end()), [](const auto module) { return std::string{module.name()}; });
     return res;
 }
 
@@ -392,39 +311,49 @@
 {
     using namespace std::string_view_literals;
     std::set<ModuleNodePair> res;
-    std::vector<libyang::S_Schema_Node> nodes;
+    std::vector<libyang::ChildInstanstiables> nodeCollections;
     std::string topLevelModule;
 
     if (path.type() == typeid(module_)) {
-        nodes = m_context->get_module(boost::get<module_>(path).m_name.c_str())->data_instantiables(0);
+        nodeCollections.emplace_back(m_context.getModule(boost::get<module_>(path).m_name.c_str())->childInstantiables());
     } else {
         auto schemaPath = anyPathToSchemaPath(path);
         if (schemaPath.m_nodes.empty()) {
-            nodes = m_context->data_instantiables(0);
+            for (const auto& module : m_context.modules()) {
+                if (module.implemented()) {
+                    nodeCollections.emplace_back(module.childInstantiables());
+                }
+            }
         } else {
             const auto pathString = pathToSchemaString(schemaPath, Prefixes::Always);
             const auto node = getSchemaNode(pathString);
-            nodes = node->child_instantiables(0);
+            nodeCollections.emplace_back(node->childInstantiables());
             topLevelModule = schemaPath.m_nodes.begin()->m_prefix->m_name;
         }
     }
 
-    for (const auto& node : nodes) {
-        if (node->module()->name() == "ietf-yang-library"sv) {
-            continue;
-        }
+    for (const auto& coll : nodeCollections) {
+        for (const auto& node : coll) {
+            if (node.module().name() == "ietf-yang-library"sv) {
+                continue;
+            }
 
-        if (recursion == Recursion::Recursive) {
-            for (auto it : node->tree_dfs()) {
-                res.insert(ModuleNodePair(boost::none, it->path(LYS_PATH_FIRST_PREFIX)));
+            if (node.module().name() == "ietf-yang-schema-mount"sv) {
+                continue;
             }
-        } else {
-            ModuleNodePair toInsert;
-            if (topLevelModule.empty() || topLevelModule != node->module()->name()) {
-                toInsert.first = node->module()->type() == 0 ? node->module()->name() : libyang::Submodule(node->module()).belongsto()->name();
+
+            if (recursion == Recursion::Recursive) {
+                for (auto it : node.childrenDfs()) {
+                    res.insert(ModuleNodePair(boost::none, it.path()));
+                }
+            } else {
+                ModuleNodePair toInsert;
+                if (topLevelModule.empty() || topLevelModule != node.module().name()) {
+                    toInsert.first = std::string{node.module().name()};
+                }
+                toInsert.second = node.name();
+                res.insert(toInsert);
             }
-            toInsert.second = node->name();
-            res.insert(toInsert);
         }
     }
 
@@ -433,71 +362,81 @@
 
 void YangSchema::loadModule(const std::string& moduleName)
 {
-    m_context->load_module(moduleName.c_str());
+    m_context.loadModule(moduleName.c_str());
 }
 
-void YangSchema::enableFeature(const std::string& moduleName, const std::string& featureName)
+void YangSchema::setEnabledFeatures(const std::string& moduleName, const std::vector<std::string>& features)
 {
     using namespace std::string_literals;
     auto module = getYangModule(moduleName);
     if (!module) {
         throw std::runtime_error("Module \""s + moduleName + "\" doesn't exist.");
     }
-    if (module->feature_enable(featureName.c_str())) {
-        throw std::runtime_error("Can't enable feature \""s + featureName + "\" for module \"" + moduleName + "\".");
+    try {
+        module->setImplemented(features);
+    } catch (libyang::ErrorWithCode&) {
+        throw std::runtime_error("Can't enable features for module \"" + moduleName + "\".");
     }
 }
 
 void YangSchema::registerModuleCallback(const std::function<std::string(const char*, const char*, const char*, const char*)>& clb)
 {
-    auto lambda = [clb](const char* mod_name, const char* mod_revision, const char* submod_name, const char* submod_revision) {
+    auto lambda = [clb](const char* mod_name, const char* mod_revision, const char* submod_name, const char* submod_revision) -> std::optional<libyang::ModuleInfo> {
         (void)submod_revision;
         auto moduleSource = clb(mod_name, mod_revision, submod_name, submod_revision);
         if (moduleSource.empty()) {
-            return libyang::Context::mod_missing_cb_return{LYS_IN_YANG, nullptr};
+            return std::nullopt;
         }
-        return libyang::Context::mod_missing_cb_return{LYS_IN_YANG, strdup(moduleSource.c_str())};
+        return libyang::ModuleInfo {
+            .data = moduleSource.c_str(),
+            .format = libyang::SchemaFormat::YANG
+
+        };
     };
 
-    m_context->add_missing_module_callback(lambda, free);
+    m_context.registerModuleCallback(lambda);
 }
 
-std::shared_ptr<libyang::Data_Node> YangSchema::dataNodeFromPath(const std::string& path, const std::optional<const std::string> value) const
+libyang::CreatedNodes YangSchema::dataNodeFromPath(const std::string& path, const std::optional<const std::string> value) const
 {
-    return std::make_shared<libyang::Data_Node>(m_context,
-                                                path.c_str(),
-                                                value ? value.value().c_str() : nullptr,
-                                                LYD_ANYDATA_CONSTSTRING,
-                                                LYD_PATH_OPT_EDIT);
+    auto options = [this, &path, &value] {
+        // If we're creating a node without a value and it's not the "empty" type, then we also need the Opaque flag.
+        auto schema = getSchemaNode(path);
+        if (schema->nodeType() == libyang::NodeType::Leaf &&
+            schema->asLeaf().valueType().base() != libyang::LeafBaseType::Empty &&
+            !value) {
+            return std::optional<libyang::CreationOptions>{libyang::CreationOptions::Opaque};
+        }
+
+        return std::optional<libyang::CreationOptions>{};
+    }();
+    return m_context.newPath2(path.c_str(), value ? value->c_str() : nullptr, options);
 }
 
-std::shared_ptr<libyang::Module> YangSchema::getYangModule(const std::string& name)
+std::optional<libyang::Module> YangSchema::getYangModule(const std::string& name)
 {
-    return m_context->get_module(name.c_str());
+    return m_context.getModuleImplemented(name.c_str());
 }
 
 namespace {
-yang::NodeTypes impl_nodeType(const libyang::S_Schema_Node& node)
+yang::NodeTypes impl_nodeType(const libyang::SchemaNode& node)
 {
-    if (!node) {
-        throw InvalidNodeException();
-    }
-    switch (node->nodetype()) {
-    case LYS_CONTAINER:
-        return libyang::Schema_Node_Container{node}.presence() ? yang::NodeTypes::PresenceContainer : yang::NodeTypes::Container;
-    case LYS_LEAF:
+    switch (node.nodeType()) {
+    case libyang::NodeType::Container:
+        return node.asContainer().isPresence() ? yang::NodeTypes::PresenceContainer : yang::NodeTypes::Container;
+    case libyang::NodeType::Leaf:
         return yang::NodeTypes::Leaf;
-    case LYS_LIST:
+    case libyang::NodeType::List:
         return yang::NodeTypes::List;
-    case LYS_RPC:
+    case libyang::NodeType::RPC:
         return yang::NodeTypes::Rpc;
-    case LYS_ACTION:
+    case libyang::NodeType::Action:
         return yang::NodeTypes::Action;
-    case LYS_NOTIF:
+    case libyang::NodeType::Notification:
         return yang::NodeTypes::Notification;
-    case LYS_ANYXML:
+    case libyang::NodeType::AnyXML:
         return yang::NodeTypes::AnyXml;
-    case LYS_LEAFLIST:
+    case libyang::NodeType::Leaflist:
         return yang::NodeTypes::LeafList;
     default:
         throw InvalidNodeException(); // FIXME: Implement all types.
@@ -507,56 +446,57 @@
 
 yang::NodeTypes YangSchema::nodeType(const schemaPath_& location, const ModuleNodePair& node) const
 {
-    return impl_nodeType(getSchemaNode(location, node));
+    return impl_nodeType(*getSchemaNode(location, node));
 }
 
 yang::NodeTypes YangSchema::nodeType(const std::string& path) const
 {
-    return impl_nodeType(getSchemaNode(path));
+    return impl_nodeType(*getSchemaNode(path));
 }
 
 std::optional<std::string> YangSchema::description(const std::string& path) const
 {
-    auto node = getSchemaNode(path.c_str());
-    return node->dsc() ? std::optional{node->dsc()} : std::nullopt;
+    auto desc = getSchemaNode(path.c_str())->description();
+    return desc ? std::optional<std::string>{desc} : std::nullopt;
+
 }
 
 yang::Status YangSchema::status(const std::string& location) const
 {
     auto node = getSchemaNode(location.c_str());
-    if (node->flags() & LYS_STATUS_DEPRC) {
+    switch (node->status()) {
+    case libyang::Status::Deprecated:
         return yang::Status::Deprecated;
-    } else if (node->flags() & LYS_STATUS_OBSLT) {
+    case libyang::Status::Obsolete:
         return yang::Status::Obsolete;
-    } else {
+    case libyang::Status::Current:
         return yang::Status::Current;
     }
+
+    __builtin_unreachable();
 }
 
 bool YangSchema::hasInputNodes(const std::string& path) const
 {
     auto node = getSchemaNode(path.c_str());
-    if (auto type = node->nodetype(); type != LYS_ACTION && type != LYS_RPC) {
+    if (auto type = node->nodeType(); type != libyang::NodeType::Action && type != libyang::NodeType::RPC) {
         throw std::logic_error("StaticSchema::hasInputNodes called with non-RPC/action path");
     }
 
     // The first child gives the /input node and then I check whether it has a child.
-    return node->child()->child().get();
+    return node->child()->child().has_value();
 }
 
 bool YangSchema::isConfig(const std::string& path) const
 {
     auto node = getSchemaNode(path.c_str());
-    if (node->flags() & LYS_CONFIG_W) {
-        return true;
-    }
-
-    // Node can still be an input node.
-    while (node->parent()) {
-        node = node->parent();
-        if (node->nodetype() == LYS_INPUT) {
+    try {
+        if (node->config() == libyang::Config::True) {
             return true;
         }
+    } catch (libyang::Error&) {
+        // For non-data nodes (like `rpc`), the config value can't be retrieved. In this case, we'll just default to
+        // "false".
     }
 
     return false;
@@ -564,22 +504,10 @@
 
 std::optional<std::string> YangSchema::defaultValue(const std::string& leafPath) const
 {
-    libyang::Schema_Node_Leaf leaf(getSchemaNode(leafPath));
-
-    if (auto leafDefault = leaf.dflt()) {
-        return leafDefault;
-    }
-
-    for (auto type = leaf.type()->der(); type != nullptr; type = type->type()->der()) {
-        if (auto defaultValue = type->dflt()) {
-            return defaultValue;
-        }
-    }
-
-    return std::nullopt;
+    return std::optional<std::string>{getSchemaNode(leafPath)->asLeaf().defaultValueStr()};
 }
 
 std::string YangSchema::dataPathToSchemaPath(const std::string& path)
 {
-    return getSchemaNode(path)->path(LYS_PATH_FIRST_PREFIX);
+    return std::string{getSchemaNode(path)->path()};
 }
diff --git a/src/yang_schema.hpp b/src/yang_schema.hpp
index dc45097..82d9acf 100644
--- a/src/yang_schema.hpp
+++ b/src/yang_schema.hpp
@@ -9,26 +9,19 @@
 #pragma once
 
 #include <functional>
+#include <libyang-cpp/Context.hpp>
 #include <optional>
 #include <set>
 #include "ast_path.hpp"
 #include "schema.hpp"
 
-namespace libyang {
-class Context;
-class Schema_Node;
-class Schema_Node_Leaf;
-class Data_Node;
-class Module;
-}
-
 /*! \class YangSchema
  *     \brief A schema class, which uses libyang for queries.
  *         */
 class YangSchema : public Schema {
 public:
     YangSchema();
-    YangSchema(std::shared_ptr<libyang::Context> lyCtx);
+    YangSchema(libyang::Context lyCtx);
     ~YangSchema() override;
 
     [[nodiscard]] yang::NodeTypes nodeType(const std::string& path) const override;
@@ -54,8 +47,8 @@
     /** @short Loads a module called moduleName. */
     void loadModule(const std::string& moduleName);
 
-    /** @short Enables a feature called featureName on a module called moduleName. */
-    void enableFeature(const std::string& moduleName, const std::string& featureName);
+    /** @short Sets enabled features. */
+    void setEnabledFeatures(const std::string& moduleName, const std::vector<std::string>& features);
 
     /** @short Adds a new module passed as a YANG string. */
     void addSchemaString(const char* schema);
@@ -67,26 +60,26 @@
     void addSchemaDirectory(const char* directoryName);
 
     /** @short Creates a new data node from a path (to be used with NETCONF edit-config) */
-    [[nodiscard]] std::shared_ptr<libyang::Data_Node> dataNodeFromPath(const std::string& path, const std::optional<const std::string> value = std::nullopt) const;
-    std::shared_ptr<libyang::Module> getYangModule(const std::string& name);
+    [[nodiscard]] libyang::CreatedNodes dataNodeFromPath(const std::string& path, const std::optional<const std::string> value = std::nullopt) const;
+    std::optional<libyang::Module> getYangModule(const std::string& name);
 
     [[nodiscard]] std::string dataPathToSchemaPath(const std::string& path);
 
 private:
     friend class YangAccess;
     template <typename NodeType>
-    [[nodiscard]] yang::TypeInfo impl_leafType(const std::shared_ptr<libyang::Schema_Node>& node) const;
+    [[nodiscard]] yang::TypeInfo impl_leafType(const NodeType& node) const;
     [[nodiscard]] std::set<std::string> modules() const;
 
 
-    /** @short Returns a single Schema_Node if the criteria matches only one, otherwise nullptr. */
-    [[nodiscard]] std::shared_ptr<libyang::Schema_Node> getSchemaNode(const std::string& node) const;
-    /** @short Returns a single Schema_Node if the criteria matches only one, otherwise nullptr. */
-    [[nodiscard]] std::shared_ptr<libyang::Schema_Node> getSchemaNode(const schemaPath_& listPath) const;
+    /** @short Returns a single SchemaNode if the criteria matches only one, otherwise nullopt. */
+    [[nodiscard]] std::optional<libyang::SchemaNode> getSchemaNode(const std::string& node) const;
+    /** @short Returns a single Schema_Node if the criteria matches only one, otherwise nullopt. */
+    [[nodiscard]] std::optional<libyang::SchemaNode> getSchemaNode(const schemaPath_& listPath) const;
 
     /** @short Returns a single Schema_Node if the criteria matches only one, otherwise nullptr. */
-    [[nodiscard]] std::shared_ptr<libyang::Schema_Node> getSchemaNode(const schemaPath_& location, const ModuleNodePair& node) const;
-    std::shared_ptr<libyang::Context> m_context;
+    [[nodiscard]] std::optional<libyang::SchemaNode> getSchemaNode(const schemaPath_& location, const ModuleNodePair& node) const;
+    libyang::Context m_context;
 
-    [[nodiscard]] std::shared_ptr<libyang::Schema_Node> impl_getSchemaNode(const std::string& node) const;
+    [[nodiscard]] std::optional<libyang::SchemaNode> impl_getSchemaNode(const std::string& node) const;
 };
diff --git a/submodules/dependencies b/submodules/dependencies
index 61e31aa..0c71c1d 160000
--- a/submodules/dependencies
+++ b/submodules/dependencies
@@ -1 +1 @@
-Subproject commit 61e31aa3927fcf29612de53a790c44d77d56a1fa
+Subproject commit 0c71c1d0c36238515418c901e169a95dc2321936
diff --git a/tests/cleanup_datastore.bash.in b/tests/cleanup_datastore.bash.in
index f5f71ac..7fdd986 100755
--- a/tests/cleanup_datastore.bash.in
+++ b/tests/cleanup_datastore.bash.in
@@ -28,8 +28,8 @@
     # https://stackoverflow.com/a/41613532
     tail --pid="$NETOPEER_PID" -f /dev/null
 
-    rm "$NETOPEER_SOCKET"
+    rm -f "$NETOPEER_SOCKET"
 fi
 
-rm -r "$SYSREPO_REPOSITORY_PATH"
+rm -rf "$SYSREPO_REPOSITORY_PATH"
 rm -rf "/dev/shm/$SYSREPO_SHM_PREFIX"*
diff --git a/tests/data_query.cpp b/tests/data_query.cpp
index 64250da..642a0e0 100644
--- a/tests/data_query.cpp
+++ b/tests/data_query.cpp
@@ -29,9 +29,8 @@
 {
     trompeloeil::sequence seq1;
     {
-        auto conn = std::make_shared<sysrepo::Connection>();
-        auto sess = std::make_shared<sysrepo::Session>(conn);
-        sess->copy_config(SR_DS_STARTUP, "example-schema", 1000, true);
+        auto sess = sysrepo::Connection{}.sessionStart();
+        sess.copyConfig(sysrepo::Datastore::Startup, "example-schema", std::chrono::milliseconds{1000});
     }
     SysrepoSubscription subscriptionExample("example-schema");
     SysrepoSubscription subscriptionOther("other-module");
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index bbcae25..8a7a9f5 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -8,6 +8,8 @@
 
 #include "trompeloeil_doctest.hpp"
 #include <sysrepo-cpp/Session.hpp>
+#include <sysrepo-cpp/utils/exception.hpp>
+#include <sysrepo-cpp/utils/utils.hpp>
 #include "proxy_datastore.hpp"
 #include "yang_schema.hpp"
 
@@ -15,9 +17,9 @@
 #include "sysrepo_access.hpp"
 using OnInvalidSchemaPathCreate = DatastoreException;
 using OnInvalidSchemaPathDelete = DatastoreException;
-using OnInvalidSchemaPathMove = sysrepo::sysrepo_exception;
+using OnInvalidSchemaPathMove = sysrepo::ErrorWithCode;
 using OnInvalidRpcPath = std::runtime_error;
-using OnInvalidRpcInput = sysrepo::sysrepo_exception;
+using OnInvalidRpcInput = sysrepo::ErrorWithCode;
 using OnKeyNotFound = void;
 using OnExec = void;
 #elif defined(netconf_BACKEND)
@@ -51,7 +53,7 @@
 
 class MockRecorder : public trompeloeil::mock_interface<Recorder> {
 public:
-    IMPLEMENT_MOCK3(write);
+    IMPLEMENT_MOCK5(write);
 };
 
 class MockDataSupplier : public trompeloeil::mock_interface<DataSupplier> {
@@ -73,8 +75,8 @@
         REQUIRE_THROWS_AS(what(), std::logic_error);
     } else if constexpr (std::is_same<Exception, DatastoreException>()) {
         REQUIRE_THROWS_AS(what(), DatastoreException);
-    } else if constexpr (std::is_same<Exception, sysrepo::sysrepo_exception>()) {
-        REQUIRE_THROWS_AS(what(), sysrepo::sysrepo_exception);
+    } else if constexpr (std::is_same<Exception, sysrepo::ErrorWithCode>()) {
+        REQUIRE_THROWS_AS(what(), sysrepo::ErrorWithCode);
     } else {
         static_assert(always_false<Exception>); // https://stackoverflow.com/a/53945549/2245623
     }
@@ -111,17 +113,15 @@
 
 TEST_CASE("setting/getting values")
 {
-    sr_log_stderr(SR_LL_DBG);
+    sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information);
     trompeloeil::sequence seq1;
     MockRecorder mockRunning;
     MockRecorder mockStartup;
-    {
-        auto conn = std::make_shared<sysrepo::Connection>();
-        auto sess = std::make_shared<sysrepo::Session>(conn);
-        sess->copy_config(SR_DS_STARTUP, "example-schema", 1000, true);
-    }
+
+    sysrepo::Connection{}.sessionStart().copyConfig(sysrepo::Datastore::Startup, "example-schema", std::chrono::milliseconds(1000));
+
     SysrepoSubscription subRunning("example-schema", &mockRunning);
-    SysrepoSubscription subStartup("example-schema", &mockStartup, SR_DS_STARTUP);
+    SysrepoSubscription subStartup("example-schema", &mockStartup, sysrepo::Datastore::Startup);
 
 #ifdef sysrepo_BACKEND
     SysrepoAccess datastore;
@@ -139,83 +139,83 @@
 
     SECTION("set leafInt8 to -128")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafInt8", std::nullopt, "-128"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafInt8", std::nullopt, "-128"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafInt8", int8_t{-128});
         datastore.commitChanges();
     }
 
     SECTION("set leafInt16 to -32768")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafInt16", std::nullopt, "-32768"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafInt16", std::nullopt, "-32768"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafInt16", int16_t{-32768});
         datastore.commitChanges();
     }
 
     SECTION("set leafInt32 to -2147483648")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafInt32", std::nullopt, "-2147483648"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafInt32", std::nullopt, "-2147483648"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafInt32", int32_t{-2147483648});
         datastore.commitChanges();
     }
 
     SECTION("set leafInt64 to -50000000000")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafInt64", std::nullopt, "-50000000000"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafInt64", std::nullopt, "-50000000000"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafInt64", int64_t{-50000000000});
         datastore.commitChanges();
     }
 
     SECTION("set leafUInt8 to 255")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafUInt8", std::nullopt, "255"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafUInt8", std::nullopt, "255"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafUInt8", uint8_t{255});
         datastore.commitChanges();
     }
 
     SECTION("set leafUInt16 to 65535")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafUInt16", std::nullopt, "65535"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafUInt16", std::nullopt, "65535"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafUInt16", uint16_t{65535});
         datastore.commitChanges();
     }
 
     SECTION("set leafUInt32 to 4294967295")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafUInt32", std::nullopt, "4294967295"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafUInt32", std::nullopt, "4294967295"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafUInt32", uint32_t{4294967295});
         datastore.commitChanges();
     }
 
     SECTION("set leafUInt64 to 50000000000")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafUInt64", std::nullopt, "50000000000"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafUInt64", std::nullopt, "50000000000"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafUInt64", uint64_t{50000000000});
         datastore.commitChanges();
     }
 
     SECTION("set leafEnum to coze")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafEnum", std::nullopt, "coze"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafEnum", std::nullopt, "coze"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafEnum", enum_{"coze"});
         datastore.commitChanges();
     }
 
     SECTION("set leafDecimal to 123.544")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafDecimal", std::nullopt, "123.544"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafDecimal", std::nullopt, "123.544"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafDecimal", 123.544);
         datastore.commitChanges();
     }
 
     SECTION("set a string, then delete it")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafString", std::nullopt, "blah"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafString", std::nullopt, "blah"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafString", "blah"s);
         datastore.commitChanges();
         DatastoreAccess::Tree expected{{"/example-schema:leafString", "blah"s}};
         REQUIRE(datastore.getItems("/example-schema:leafString") == expected);
 
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafString", "blah"s, std::nullopt));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Deleted, "/example-schema:leafString", "blah"s, std::nullopt, std::nullopt));
         datastore.deleteItem("/example-schema:leafString");
         datastore.commitChanges();
         expected.clear();
@@ -224,7 +224,7 @@
 
     SECTION("set a string, then set it to something else without commiting")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafString", std::nullopt, "oops"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafString", std::nullopt, "oops"s, std::nullopt));
         datastore.setLeaf("/example-schema:leafString", "blah"s);
         datastore.setLeaf("/example-schema:leafString", "oops"s);
         datastore.commitChanges();
@@ -243,7 +243,7 @@
     SECTION("create presence container")
     {
         REQUIRE(datastore.dump(DataFormat::Json).find("example-schema:pContainer") == std::string::npos);
-        REQUIRE_CALL(mockRunning, write("/example-schema:pContainer", std::nullopt, ""s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:pContainer", std::nullopt, std::nullopt, std::nullopt));
         datastore.createItem("/example-schema:pContainer");
         datastore.commitChanges();
         REQUIRE(datastore.dump(DataFormat::Json).find("example-schema:pContainer") != std::string::npos);
@@ -252,14 +252,14 @@
     SECTION("create/delete a list instance")
     {
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Nguyen']", std::nullopt, ""s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Nguyen']/name", std::nullopt, "Nguyen"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Nguyen']", std::nullopt, std::nullopt, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Nguyen']/name", std::nullopt, "Nguyen"s, std::nullopt));
             datastore.createItem("/example-schema:person[name='Nguyen']");
             datastore.commitChanges();
         }
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Nguyen']", ""s, std::nullopt));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Nguyen']/name", "Nguyen"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Deleted, "/example-schema:person[name='Nguyen']", std::nullopt, std::nullopt, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Deleted, "/example-schema:person[name='Nguyen']/name", "Nguyen"s, std::nullopt, std::nullopt));
             datastore.deleteItem("/example-schema:person[name='Nguyen']");
             datastore.commitChanges();
         }
@@ -288,12 +288,12 @@
     SECTION("leafref pointing to a key of a list")
     {
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Dan']", std::nullopt, ""s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Dan']/name", std::nullopt, "Dan"s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Elfi']", std::nullopt, ""s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Elfi']/name", std::nullopt, "Elfi"s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Kolafa']", std::nullopt, ""s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Kolafa']/name", std::nullopt, "Kolafa"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Dan']", std::nullopt, std::nullopt, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Dan']/name", std::nullopt, "Dan"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Elfi']", std::nullopt, std::nullopt, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Elfi']/name", std::nullopt, "Elfi"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Kolafa']", std::nullopt, std::nullopt, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Kolafa']/name", std::nullopt, "Kolafa"s, std::nullopt));
             datastore.createItem("/example-schema:person[name='Dan']");
             datastore.createItem("/example-schema:person[name='Elfi']");
             datastore.createItem("/example-schema:person[name='Kolafa']");
@@ -318,15 +318,16 @@
 
         datastore.setLeaf("/example-schema:bossPerson", value);
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:bossPerson", std::nullopt, value));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:bossPerson", std::nullopt, value, std::nullopt));
             datastore.commitChanges();
         }
         REQUIRE(datastore.getItems("/example-schema:bossPerson") == DatastoreAccess::Tree{{"/example-schema:bossPerson", value}});
     }
+
     SECTION("bool values get correctly represented as bools")
     {
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:down", std::nullopt, "true"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:down", std::nullopt, "true"s, std::nullopt));
             datastore.setLeaf("/example-schema:down", bool{true});
             datastore.commitChanges();
         }
@@ -338,8 +339,8 @@
     SECTION("getting items from the whole module")
     {
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:up", std::nullopt, "true"s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:down", std::nullopt, "false"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:up", std::nullopt, "true"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:down", std::nullopt, "false"s, std::nullopt));
             datastore.setLeaf("/example-schema:up", bool{true});
             datastore.setLeaf("/example-schema:down", bool{false});
             datastore.commitChanges();
@@ -355,7 +356,7 @@
     SECTION("getItems returns correct datatypes")
     {
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:leafEnum", std::nullopt, "lol"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafEnum", std::nullopt, "lol"s, std::nullopt));
             datastore.setLeaf("/example-schema:leafEnum", enum_{"lol"});
             datastore.commitChanges();
         }
@@ -367,12 +368,12 @@
     SECTION("getItems on a list")
     {
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Jan']", std::nullopt, ""s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Jan']/name", std::nullopt, "Jan"s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Michal']", std::nullopt, ""s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Michal']/name", std::nullopt, "Michal"s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Petr']", std::nullopt, ""s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:person[name='Petr']/name", std::nullopt, "Petr"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Jan']", std::nullopt, std::nullopt, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Jan']/name", std::nullopt, "Jan"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Michal']", std::nullopt, std::nullopt, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Michal']/name", std::nullopt, "Michal"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Petr']", std::nullopt, std::nullopt, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:person[name='Petr']/name", std::nullopt, "Petr"s, std::nullopt));
             datastore.createItem("/example-schema:person[name='Jan']");
             datastore.createItem("/example-schema:person[name='Michal']");
             datastore.createItem("/example-schema:person[name='Petr']");
@@ -397,7 +398,7 @@
         REQUIRE(datastore.getItems("/example-schema:pContainer") == expected);
 
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:pContainer", std::nullopt, ""s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:pContainer", std::nullopt, std::nullopt, std::nullopt));
             datastore.createItem("/example-schema:pContainer");
             datastore.commitChanges();
         }
@@ -408,7 +409,7 @@
 
         // Make sure it's not there after we delete it
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:pContainer", ""s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Deleted, "/example-schema:pContainer", std::nullopt, std::nullopt, std::nullopt));
             datastore.deleteItem("/example-schema:pContainer");
             datastore.commitChanges();
         }
@@ -438,7 +439,7 @@
         // Make sure it's not there before we create it
         REQUIRE(datastore.getItems("/example-schema:inventory/stuff") == expected);
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:inventory/stuff", std::nullopt, ""s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:inventory/stuff", std::nullopt, std::nullopt, std::nullopt));
             datastore.createItem("/example-schema:inventory/stuff");
             datastore.commitChanges();
         }
@@ -447,7 +448,7 @@
         };
         REQUIRE(datastore.getItems("/example-schema:inventory/stuff") == expected);
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:inventory/stuff", ""s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Deleted, "/example-schema:inventory/stuff", std::nullopt, std::nullopt, std::nullopt));
             datastore.deleteItem("/example-schema:inventory/stuff");
             datastore.commitChanges();
         }
@@ -458,7 +459,7 @@
     SECTION("floats")
     {
         datastore.setLeaf("/example-schema:leafDecimal", 123.4);
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafDecimal", std::nullopt, "123.4"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafDecimal", std::nullopt, "123.4"s, std::nullopt));
         datastore.commitChanges();
         DatastoreAccess::Tree expected{
             {"/example-schema:leafDecimal", 123.4},
@@ -469,7 +470,7 @@
     SECTION("unions")
     {
         datastore.setLeaf("/example-schema:unionIntString", int32_t{10});
-        REQUIRE_CALL(mockRunning, write("/example-schema:unionIntString", std::nullopt, "10"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:unionIntString", std::nullopt, "10"s, std::nullopt));
         datastore.commitChanges();
         DatastoreAccess::Tree expected{
             {"/example-schema:unionIntString", int32_t{10}},
@@ -480,7 +481,7 @@
     SECTION("identityref")
     {
         datastore.setLeaf("/example-schema:beast", identityRef_{"example-schema", "Mammal"});
-        REQUIRE_CALL(mockRunning, write("/example-schema:beast", std::nullopt, "example-schema:Mammal"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:beast", std::nullopt, "example-schema:Mammal"s, std::nullopt));
         datastore.commitChanges();
         DatastoreAccess::Tree expected{
             {"/example-schema:beast", identityRef_{"example-schema", "Mammal"}},
@@ -488,7 +489,7 @@
         REQUIRE(datastore.getItems("/example-schema:beast") == expected);
 
         datastore.setLeaf("/example-schema:beast", identityRef_{"Whale"});
-        REQUIRE_CALL(mockRunning, write("/example-schema:beast", "example-schema:Mammal", "example-schema:Whale"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Modified, "/example-schema:beast", "example-schema:Mammal", "example-schema:Whale"s, std::nullopt));
         datastore.commitChanges();
         expected = {
             {"/example-schema:beast", identityRef_{"example-schema", "Whale"}},
@@ -499,7 +500,7 @@
     SECTION("binary")
     {
         datastore.setLeaf("/example-schema:blob", binary_{"cHduegByIQ=="s});
-        REQUIRE_CALL(mockRunning, write("/example-schema:blob", std::nullopt, "cHduegByIQ=="s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:blob", std::nullopt, "cHduegByIQ=="s, std::nullopt));
         datastore.commitChanges();
         DatastoreAccess::Tree expected{
             {"/example-schema:blob", binary_{"cHduegByIQ=="s}},
@@ -510,7 +511,7 @@
     SECTION("empty")
     {
         datastore.setLeaf("/example-schema:dummy", empty_{});
-        REQUIRE_CALL(mockRunning, write("/example-schema:dummy", std::nullopt, ""s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:dummy", std::nullopt, ""s, std::nullopt));
         datastore.commitChanges();
         DatastoreAccess::Tree expected{
             {"/example-schema:dummy", empty_{}},
@@ -521,7 +522,7 @@
     SECTION("bits")
     {
         datastore.setLeaf("/example-schema:flags", bits_{{"sign", "carry"}});
-        REQUIRE_CALL(mockRunning, write("/example-schema:flags", std::nullopt, "carry sign"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:flags", std::nullopt, "carry sign"s, std::nullopt));
         datastore.commitChanges();
         DatastoreAccess::Tree expected{
             {"/example-schema:flags", bits_{{"carry", "sign"}}},
@@ -564,8 +565,8 @@
     SECTION("leaf list")
     {
         DatastoreAccess::Tree expected;
-        REQUIRE_CALL(mockRunning, write("/example-schema:addresses", std::nullopt, "0.0.0.0"s));
-        REQUIRE_CALL(mockRunning, write("/example-schema:addresses", std::nullopt, "127.0.0.1"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:addresses[.='0.0.0.0']", std::nullopt, "0.0.0.0"s, std::nullopt));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:addresses[.='127.0.0.1']", std::nullopt, "127.0.0.1"s, std::nullopt));
         datastore.createItem("/example-schema:addresses[.='0.0.0.0']");
         datastore.createItem("/example-schema:addresses[.='127.0.0.1']");
         datastore.commitChanges();
@@ -576,7 +577,7 @@
         };
         REQUIRE(datastore.getItems("/example-schema:addresses") == expected);
 
-        REQUIRE_CALL(mockRunning, write("/example-schema:addresses", "0.0.0.0"s, std::nullopt));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Deleted, "/example-schema:addresses[.='0.0.0.0']", "0.0.0.0"s, std::nullopt, std::nullopt));
         datastore.deleteItem("/example-schema:addresses[.='0.0.0.0']");
         datastore.commitChanges();
         expected = {
@@ -585,7 +586,7 @@
         };
         REQUIRE(datastore.getItems("/example-schema:addresses") == expected);
 
-        REQUIRE_CALL(mockRunning, write("/example-schema:addresses", "127.0.0.1"s, std::nullopt));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Deleted, "/example-schema:addresses[.='127.0.0.1']", "127.0.0.1"s, std::nullopt, std::nullopt));
         datastore.deleteItem("/example-schema:addresses[.='127.0.0.1']");
         datastore.commitChanges();
         expected = {};
@@ -617,12 +618,12 @@
     {
         {
             REQUIRE(datastore.getItems("/example-schema:leafInt16") == DatastoreAccess::Tree{});
-            REQUIRE_CALL(mockRunning, write("/example-schema:leafInt16", std::nullopt, "123"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafInt16", std::nullopt, "123"s, std::nullopt));
             datastore.setLeaf("/example-schema:leafInt16", int16_t{123});
             datastore.commitChanges();
         }
         REQUIRE(datastore.getItems("/example-schema:leafInt16") == DatastoreAccess::Tree{{"/example-schema:leafInt16", int16_t{123}}});
-        REQUIRE_CALL(mockRunning, write("/example-schema:leafInt16", "123"s, std::nullopt));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Deleted, "/example-schema:leafInt16", "123"s, std::nullopt, std::nullopt));
         datastore.copyConfig(Datastore::Startup, Datastore::Running);
         REQUIRE(datastore.getItems("/example-schema:leafInt16") == DatastoreAccess::Tree{});
     }
@@ -631,13 +632,9 @@
     {
         DatastoreAccess::Tree expected;
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:protocols", std::nullopt, "http"s));
-            // FIXME: Why no notifications for these??
-            // ... possibly because my subscription doesn't extract it properly?
-            // REQUIRE_CALL(mock, write("/example-schema:protocols", std::nullopt, "ftp"s));
-            // REQUIRE_CALL(mock, write("/example-schema:protocols", std::nullopt, "pop3"s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:protocols", "http"s, "ftp"s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:protocols", "ftp"s, "pop3"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:protocols[.='http']", "", "http"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:protocols[.='ftp']", "http"s, "ftp"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:protocols[.='pop3']", "ftp"s, "pop3"s, std::nullopt));
             datastore.createItem("/example-schema:protocols[.='http']");
             datastore.createItem("/example-schema:protocols[.='ftp']");
             datastore.createItem("/example-schema:protocols[.='pop3']");
@@ -654,7 +651,7 @@
         std::string sourcePath;
         SECTION("begin")
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:protocols", std::nullopt, "pop3"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:protocols[.='pop3']", ""s, "pop3"s, std::nullopt));
             sourcePath = "/example-schema:protocols[.='pop3']";
             datastore.moveItem(sourcePath, yang::move::Absolute::Begin);
             datastore.commitChanges();
@@ -670,7 +667,16 @@
         SECTION("end")
         {
             sourcePath = "/example-schema:protocols[.='http']";
-            REQUIRE_CALL(mockRunning, write("/example-schema:protocols", "pop3"s, "http"s));
+
+#if defined(yang_BACKEND) || defined(netconf_BACKEND)
+            // Due to the libyang diff algorithm being imperfect, the move operations differ between backends.
+            // The same applies for the stuff below.
+            // https://github.com/sysrepo/sysrepo/issues/2732
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:protocols[.='ftp']", ""s, "ftp"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:protocols[.='pop3']", "ftp"s, "pop3"s, std::nullopt));
+#else
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:protocols[.='http']", "pop3"s, "http"s, std::nullopt));
+#endif
             datastore.moveItem(sourcePath, yang::move::Absolute::End);
             datastore.commitChanges();
             expected = {
@@ -685,7 +691,12 @@
         SECTION("after")
         {
             sourcePath = "/example-schema:protocols[.='http']";
-            REQUIRE_CALL(mockRunning, write("/example-schema:protocols", "ftp"s, "http"s));
+#if defined(yang_BACKEND) || defined(netconf_BACKEND)
+            // see the test for "end" for explanation if this #ifdef
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:protocols[.='ftp']", ""s, "ftp"s, std::nullopt));
+#else
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:protocols[.='http']", "ftp"s, "http"s, std::nullopt));
+#endif
             datastore.moveItem(sourcePath, yang::move::Relative{yang::move::Relative::Position::After, {{".", "ftp"s}}});
             datastore.commitChanges();
             expected = {
@@ -700,7 +711,12 @@
         SECTION("before")
         {
             sourcePath = "/example-schema:protocols[.='http']";
-            REQUIRE_CALL(mockRunning, write("/example-schema:protocols", "ftp"s, "http"s));
+#if defined(yang_BACKEND) || defined(netconf_BACKEND)
+            // see the test for "end" for explanation if this #ifdef
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:protocols[.='ftp']", ""s, "ftp"s, std::nullopt));
+#else
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:protocols[.='http']", "ftp"s, "http"s, std::nullopt));
+#endif
             datastore.moveItem(sourcePath, yang::move::Relative{yang::move::Relative::Position::Before, {{".", "pop3"s}}});
             datastore.commitChanges();
             expected = {
@@ -725,12 +741,12 @@
     {
         DatastoreAccess::Tree expected;
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:players[name='John']", std::nullopt, ""s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:players[name='John']/name", std::nullopt, "John"s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:players[name='Eve']", ""s, ""s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:players[name='Eve']/name", std::nullopt, "Eve"s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:players[name='Adam']", ""s, ""s));
-            REQUIRE_CALL(mockRunning, write("/example-schema:players[name='Adam']/name", std::nullopt, "Adam"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:players[name='John']", std::nullopt, std::nullopt, ""s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:players[name='John']/name", std::nullopt, "John"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:players[name='Eve']", std::nullopt, std::nullopt, "[name='John']"));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:players[name='Eve']/name", std::nullopt, "Eve"s, std::nullopt));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:players[name='Adam']", std::nullopt, std::nullopt, "[name='Eve']"));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:players[name='Adam']/name", std::nullopt, "Adam"s, std::nullopt));
             datastore.createItem("/example-schema:players[name='John']");
             datastore.createItem("/example-schema:players[name='Eve']");
             datastore.createItem("/example-schema:players[name='Adam']");
@@ -750,7 +766,7 @@
         SECTION("begin")
         {
             sourcePath = "/example-schema:players[name='Adam']";
-            REQUIRE_CALL(mockRunning, write("/example-schema:players[name='Adam']", std::nullopt, ""s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:players[name='Adam']", std::nullopt, std::nullopt, ""s));
             datastore.moveItem(sourcePath, yang::move::Absolute::Begin);
             datastore.commitChanges();
             expected = {
@@ -767,7 +783,14 @@
         SECTION("end")
         {
             sourcePath = "/example-schema:players[name='John']";
-            REQUIRE_CALL(mockRunning, write("/example-schema:players[name='John']", ""s, ""s));
+#if defined(yang_BACKEND) || defined(netconf_BACKEND)
+            // TODO: see TODO comment in leaflist/end
+            // Although these make much less sense
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:players[name='Eve']", std::nullopt, std::nullopt, ""));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:players[name='Adam']", std::nullopt, std::nullopt, "[name='Eve']"));
+#else
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:players[name='John']", std::nullopt, std::nullopt, "[name='Adam']"));
+#endif
             datastore.moveItem(sourcePath, yang::move::Absolute::End);
             datastore.commitChanges();
             expected = {
@@ -784,7 +807,13 @@
         SECTION("after")
         {
             sourcePath = "/example-schema:players[name='John']";
-            REQUIRE_CALL(mockRunning, write("/example-schema:players[name='John']", ""s, ""s));
+#if defined(yang_BACKEND) || defined(netconf_BACKEND)
+            // TODO: see TODO comment in leaflist/end
+            // Although these make much less sense
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:players[name='Eve']", std::nullopt, std::nullopt, ""));
+#else
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:players[name='John']", std::nullopt, std::nullopt, "[name='Eve']"));
+#endif
             datastore.moveItem(sourcePath, yang::move::Relative{yang::move::Relative::Position::After, {{"name", "Eve"s}}});
             datastore.commitChanges();
             expected = {
@@ -801,7 +830,13 @@
         SECTION("before")
         {
             sourcePath = "/example-schema:players[name='John']";
-            REQUIRE_CALL(mockRunning, write("/example-schema:players[name='John']", ""s, ""s));
+#if defined(yang_BACKEND) || defined(netconf_BACKEND)
+            // TODO: see TODO comment in leaflist/end
+            // Although these make much less sense
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:players[name='Eve']", std::nullopt, std::nullopt, ""));
+#else
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Moved, "/example-schema:players[name='John']", std::nullopt, std::nullopt, "[name='Eve']"));
+#endif
             datastore.moveItem(sourcePath, yang::move::Relative{yang::move::Relative::Position::Before, {{"name", "Adam"s}}});
             datastore.commitChanges();
             expected = {
@@ -819,7 +854,7 @@
     SECTION("getting /")
     {
         {
-            REQUIRE_CALL(mockRunning, write("/example-schema:leafInt32", std::nullopt, "64"s));
+            REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:leafInt32", std::nullopt, "64"s, std::nullopt));
             datastore.setLeaf("/example-schema:leafInt32", 64);
             datastore.commitChanges();
         }
@@ -841,9 +876,9 @@
 
     SECTION("two key lists")
     {
-        REQUIRE_CALL(mockRunning, write("/example-schema:point[x='12'][y='10']", std::nullopt, ""s));
-        REQUIRE_CALL(mockRunning, write("/example-schema:point[x='12'][y='10']/x", std::nullopt, "12"s));
-        REQUIRE_CALL(mockRunning, write("/example-schema:point[x='12'][y='10']/y", std::nullopt, "10"s));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:point[x='12'][y='10']", std::nullopt, std::nullopt, std::nullopt));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:point[x='12'][y='10']/x", std::nullopt, "12"s, std::nullopt));
+        REQUIRE_CALL(mockRunning, write(sysrepo::ChangeOperation::Created, "/example-schema:point[x='12'][y='10']/y", std::nullopt, "10"s, std::nullopt));
         datastore.createItem("/example-schema:point[x='12'][y='10']");
         datastore.commitChanges();
         REQUIRE(datastore.dump(DataFormat::Json).find("example-schema:point") != std::string::npos);
@@ -853,69 +888,74 @@
 }
 
 struct ActionCb {
-    int operator()(sysrepo::S_Session session,
-            const char* xpath,
-            [[maybe_unused]] const sysrepo::S_Vals input,
-            [[maybe_unused]] sr_event_t event,
-            [[maybe_unused]] uint32_t request_id,
-            sysrepo::S_Vals_Holder output)
+    sysrepo::ErrorCode operator()(
+        [[maybe_unused]] sysrepo::Session session,
+        [[maybe_unused]] uint32_t subscriptionId,
+        std::string_view xpath,
+        [[maybe_unused]] const libyang::DataNode input,
+        [[maybe_unused]] sysrepo::Event event,
+        [[maybe_unused]] uint32_t requestId,
+        libyang::DataNode output)
     {
-        if (session->get_context()->get_node(nullptr, xpath)->path(LYS_PATH_FIRST_PREFIX) == "/example-schema:ports/shutdown") {
-            auto buf = output->allocate(1);
-            buf->val(0)->set(joinPaths(xpath, "success").c_str(), true);
-            return SR_ERR_OK;
+        if (session.getContext().findPath(xpath.data()).path() == "/example-schema:ports/shutdown") {
+            // `xpath` holds the subscription xpath which won't have list keys. We need the path with list keys and
+            // we'll find that in the input.
+            auto inputPath = input.findXPath("/example-schema:ports/shutdown").front().path();
+            output.newPath(joinPaths(std::string{inputPath}, "success").c_str(), "true", libyang::CreationOptions::Output);
+            return sysrepo::ErrorCode::Ok;
         }
         throw std::runtime_error("unrecognized RPC");
     }
 };
 
 struct RpcCb {
-    int operator()([[maybe_unused]] sysrepo::S_Session session,
-            const char* xpath,
-            const sysrepo::S_Vals input,
-            [[maybe_unused]] sr_event_t event,
-            [[maybe_unused]] uint32_t request_id,
-            sysrepo::S_Vals_Holder output)
+    sysrepo::ErrorCode operator()(
+        [[maybe_unused]] sysrepo::Session session,
+        [[maybe_unused]] uint32_t subscriptionId,
+        std::string_view xpath,
+        const libyang::DataNode input,
+        [[maybe_unused]] sysrepo::Event event,
+        [[maybe_unused]] uint32_t requestId,
+        libyang::DataNode output)
     {
         const auto nukes = "/example-schema:launch-nukes"s;
         if (xpath == "/example-schema:noop"s || xpath == "/example-schema:fire"s) {
-            return SR_ERR_OK;
+            return sysrepo::ErrorCode::Ok;
         }
 
         if (xpath == nukes) {
             uint64_t kilotons = 0;
             bool hasCities = false;
-            for (size_t i = 0; i < input->val_cnt(); ++i) {
-                const auto& val = input->val(i);
-                if (val->xpath() == nukes + "/payload") {
+            for (const auto& inputNode : input.childrenDfs()) {
+                if (inputNode.path() == nukes) {
+                    continue; // ignore, top-level RPC
+                }
+                if (inputNode.path() == nukes + "/payload") {
                     continue; // ignore, container
                 }
-                if (val->xpath() == nukes + "/description") {
+                if (inputNode.path() == nukes + "/description") {
                     continue; // unused
                 }
 
-                if (val->xpath() == nukes + "/payload/kilotons") {
-                    kilotons = val->data()->get_uint64();
-                } else if (std::string_view{val->xpath()}.find(nukes + "/cities") == 0) {
+                if (inputNode.path() == nukes + "/payload/kilotons") {
+                    kilotons = std::get<uint64_t>(inputNode.asTerm().value());
+                } else if (std::string_view{inputNode.path().get().get()}.find(nukes + "/cities") == 0) {
                     hasCities = true;
                 } else {
-                    throw std::runtime_error("RPC launch-nukes: unexpected input "s + val->xpath());
+                    throw std::runtime_error("RPC launch-nukes: unexpected input "s + inputNode.path().get().get());
                 }
             }
             if (kilotons == 333'666) {
                 // magic, just do not generate any output. This is important because the NETCONF RPC returns just <ok/>.
-                return SR_ERR_OK;
+                return sysrepo::ErrorCode::Ok;
             }
-            auto buf = output->allocate(2);
-            size_t i = 0;
-            buf->val(i++)->set((nukes + "/blast-radius").c_str(), uint32_t{33'666});
-            buf->val(i++)->set((nukes + "/actual-yield").c_str(), static_cast<uint64_t>(1.33 * kilotons));
+            output.newPath((nukes + "/blast-radius").c_str(), "33666", libyang::CreationOptions::Output);
+            output.newPath((nukes + "/actual-yield").c_str(), std::to_string(static_cast<uint64_t>(1.33 * kilotons)).c_str(), libyang::CreationOptions::Output);
             if (hasCities) {
-                buf = output->reallocate(output->val_cnt() + 2);
-                buf->val(i++)->set((nukes + "/damaged-places/targets[city='London']/city").c_str(), "London");
-                buf->val(i++)->set((nukes + "/damaged-places/targets[city='Berlin']/city").c_str(), "Berlin");
+                output.newPath((nukes + "/damaged-places/targets[city='London']/city").c_str(), "London", libyang::CreationOptions::Output);
+                output.newPath((nukes + "/damaged-places/targets[city='Berlin']/city").c_str(), "Berlin", libyang::CreationOptions::Output);
             }
-            return SR_ERR_OK;
+            return sysrepo::ErrorCode::Ok;
         }
         throw std::runtime_error("unrecognized RPC");
     }
@@ -938,18 +978,14 @@
 #error "Unknown backend"
 #endif
 
-    auto srConn = std::make_shared<sysrepo::Connection>();
-    auto srSession = std::make_shared<sysrepo::Session>(srConn);
-    auto srSubscription = std::make_shared<sysrepo::Subscribe>(srSession);
-    auto rpcCb = std::make_shared<RpcCb>();
-    auto actionCb = std::make_shared<ActionCb>();
-    sysrepo::Logs{}.set_stderr(SR_LL_INF);
+    sysrepo::setLogLevelStderr(sysrepo::LogLevel::Information);
+
+    auto srSubscription = sysrepo::Connection{}.sessionStart().onRPCAction("/example-schema:noop", RpcCb{});
+    srSubscription.onRPCAction("/example-schema:launch-nukes", RpcCb{});
+    srSubscription.onRPCAction("/example-schema:fire", RpcCb{});
+    srSubscription.onRPCAction("/example-schema:ports/shutdown", ActionCb{});
+
     SysrepoSubscription subscription("example-schema", nullptr);
-    // careful here, sysrepo insists on module_change CBs being registered before RPC CBs, otherwise there's a memleak
-    srSubscription->rpc_subscribe("/example-schema:noop", RpcCb{}, 0, SR_SUBSCR_CTX_REUSE);
-    srSubscription->rpc_subscribe("/example-schema:launch-nukes", RpcCb{}, 0, SR_SUBSCR_CTX_REUSE);
-    srSubscription->rpc_subscribe("/example-schema:fire", RpcCb{}, 0, SR_SUBSCR_CTX_REUSE);
-    srSubscription->rpc_subscribe("/example-schema:ports/shutdown", ActionCb{}, 0, SR_SUBSCR_CTX_REUSE);
 
     SECTION("rpc")
     {
@@ -1086,13 +1122,12 @@
 {
     const auto testNode = "/example-schema:leafInt32";
     {
-        auto conn = std::make_shared<sysrepo::Connection>();
-        auto sess = std::make_shared<sysrepo::Session>(conn);
-        sess->delete_item(testNode);
-        sess->apply_changes(1000, 1);
-        sess->session_switch_ds(SR_DS_STARTUP);
-        sess->delete_item(testNode);
-        sess->apply_changes(1000, 1);
+        auto sess = sysrepo::Connection{}.sessionStart();
+        sess.deleteItem(testNode);
+        sess.applyChanges(std::chrono::milliseconds{1000});
+        sess.switchDatastore(sysrepo::Datastore::Startup);
+        sess.deleteItem(testNode);
+        sess.applyChanges(std::chrono::milliseconds{1000});
     }
     MockRecorder mockRunning;
     MockRecorder mockStartup;
diff --git a/tests/init_datastore.bash.in b/tests/init_datastore.bash.in
index 6eee340..d8eb2c3 100755
--- a/tests/init_datastore.bash.in
+++ b/tests/init_datastore.bash.in
@@ -22,7 +22,7 @@
 shift
 
 # Install the module
-"$SYSREPOCTL" --search-dirs "$YANG_DIR" --install "$MODULE" -a
+"$SYSREPOCTL" --search-dirs "$YANG_DIR" --install "$MODULE" -v3
 
 BACKEND="$1"
 shift
diff --git a/tests/mock/sysrepo_subscription.cpp b/tests/mock/sysrepo_subscription.cpp
index 8faffa5..d4827bb 100644
--- a/tests/mock/sysrepo_subscription.cpp
+++ b/tests/mock/sysrepo_subscription.cpp
@@ -21,29 +21,44 @@
     {
     }
 
-    int operator()(
-        sysrepo::S_Session sess,
-        [[maybe_unused]] const char* module_name,
-        [[maybe_unused]] const char* xpath,
-        [[maybe_unused]] sr_event_t event,
-        [[maybe_unused]] uint32_t request_id)
+    sysrepo::ErrorCode operator()(
+        sysrepo::Session sess,
+        uint32_t /* sub_id */,
+        std::string_view module_name,
+        std::optional<std::string_view> /* sub_xpath */,
+        sysrepo::Event event,
+        uint32_t /* request_id */)
     {
         using namespace std::string_literals;
-        if (event == SR_EV_CHANGE) {
-            return SR_ERR_OK;
+        if (event == sysrepo::Event::Change) {
+            return sysrepo::ErrorCode::Ok;
         }
 
-        auto it = sess->get_changes_iter(("/"s + module_name + ":*//.").c_str());
+        for (const auto& it : sess.getChanges(("/"s + module_name.data() + ":*//.").c_str())) {
+            auto xpath = it.node.path();
+            std::optional<std::string> oldValue;
+            std::optional<std::string> newValue;
+            if (it.operation == sysrepo::ChangeOperation::Deleted) {
+                oldValue = it.node.schema().nodeType() == libyang::NodeType::Leaf || it.node.schema().nodeType() == libyang::NodeType::Leaflist ?
+                    std::optional<std::string>{it.node.asTerm().valueStr()} :
+                    std::nullopt;
+            } else {
+                oldValue = std::optional<std::string>{it.previousValue};
+                newValue = it.node.schema().nodeType() == libyang::NodeType::Leaf || it.node.schema().nodeType() == libyang::NodeType::Leaflist ?
+                    std::optional<std::string>{it.node.asTerm().valueStr()} :
+                    std::nullopt;
 
-        while (auto change = sess->get_change_next(it)) {
-            auto xpath = (change->new_val() ? change->new_val() : change->old_val())->xpath();
+            }
+            std::optional<std::string> previousList;
 
-            auto oldValue = change->old_val() ? std::optional{change->old_val()->val_to_string()} : std::nullopt;
-            auto newValue = change->new_val() ? std::optional{change->new_val()->val_to_string()} : std::nullopt;
-            m_recorder->write(xpath, oldValue, newValue);
+            if (it.previousList) {
+                previousList = std::string{*it.previousList};
+            }
+
+            m_recorder->write(it.operation, std::string{xpath}, oldValue, newValue, previousList);
         }
 
-        return SR_ERR_OK;
+        return sysrepo::ErrorCode::Ok;
     }
 
 private:
@@ -55,111 +70,39 @@
 
 DataSupplier::~DataSupplier() = default;
 
-SysrepoSubscription::SysrepoSubscription(const std::string& moduleName, Recorder* rec, sr_datastore_t ds)
-    : m_connection(std::make_shared<sysrepo::Connection>())
+SysrepoSubscription::SysrepoSubscription(const std::string& moduleName, Recorder* rec, sysrepo::Datastore ds)
+    : m_subscription([&moduleName, &rec, ds] { // This is an immediately invoked lambda.
+        return sysrepo::Connection{}.sessionStart(ds).onModuleChange(moduleName.c_str(),
+                rec ? sysrepo::ModuleChangeCb{MyCallback{moduleName, rec}}
+                : sysrepo::ModuleChangeCb{[](auto, auto, auto, auto, auto, auto) { return sysrepo::ErrorCode::Ok; }});
+    }())
 {
-    m_session = std::make_shared<sysrepo::Session>(m_connection, ds);
-    m_subscription = std::make_shared<sysrepo::Subscribe>(m_session);
-    sysrepo::ModuleChangeCb cb;
-    if (rec) {
-        cb = MyCallback{moduleName, rec};
-    } else {
-        cb = [](auto, auto, auto, auto, auto) { return SR_ERR_OK; };
-    }
-
-    m_subscription->module_change_subscribe(moduleName.c_str(), cb);
 }
 
-
-struct leafDataToSysrepoVal {
-    leafDataToSysrepoVal(sysrepo::S_Val v, const std::string& xpath)
-        : v(v)
-        , xpath(xpath)
-    {
-    }
-
-    void operator()(const binary_& what)
-    {
-        v->set(xpath.c_str(), what.m_value.c_str(), SR_BINARY_T);
-    }
-
-    void operator()(const enum_& what)
-    {
-        v->set(xpath.c_str(), what.m_value.c_str(), SR_ENUM_T);
-    }
-
-    void operator()(const identityRef_& what)
-    {
-        v->set(xpath.c_str(), (what.m_prefix->m_name + what.m_value).c_str(), SR_IDENTITYREF_T);
-    }
-
-    void operator()(const empty_)
-    {
-        v->set(xpath.c_str(), nullptr, SR_LEAF_EMPTY_T);
-    }
-
-    void operator()(const std::string& what)
-    {
-        v->set(xpath.c_str(), what.c_str());
-    }
-
-    void operator()(const bits_& what)
-    {
-        std::stringstream ss;
-        std::copy(what.m_bits.begin(), what.m_bits.end(), std::experimental::make_ostream_joiner(ss, " "));
-        v->set(xpath.c_str(), ss.str().c_str());
-    }
-
-    template <typename Type>
-    void operator()(const Type what)
-    {
-        v->set(xpath.c_str(), what);
-    }
-
-    void operator()([[maybe_unused]] const special_ what)
-    {
-        throw std::logic_error("Attempted to create a SR val from a special_ value");
-    }
-
-    ::sysrepo::S_Val v;
-    std::string xpath;
-};
-
 class OperationalDataCallback {
 public:
     OperationalDataCallback(const DataSupplier& dataSupplier)
         : m_dataSupplier(dataSupplier)
     {
     }
-    int operator()(
-        [[maybe_unused]] sysrepo::S_Session sess,
-        [[maybe_unused]] const char* module_name,
-        const char* path,
-        [[maybe_unused]] const char* request_xpath,
-        [[maybe_unused]] uint32_t request_id,
-        libyang::S_Data_Node& parent)
+    sysrepo::ErrorCode operator()(
+            sysrepo::Session session,
+            [[maybe_unused]] uint32_t subscriptionId,
+            [[maybe_unused]] std::string_view moduleName,
+            std::optional<std::string_view> subXPath,
+            [[maybe_unused]] std::optional<std::string_view> requestXPath,
+            [[maybe_unused]] uint32_t requestId,
+            std::optional<libyang::DataNode>& output)
     {
-        auto data = m_dataSupplier.get_data(path);
-        libyang::S_Data_Node res;
+        auto data = m_dataSupplier.get_data(subXPath->data());
         for (const auto& [p, v] : data) {
-            if (!res) {
-                res = std::make_shared<libyang::Data_Node>(
-                    sess->get_context(),
-                    p.c_str(),
-                    v.type() == typeid(empty_) ? nullptr : leafDataToString(v).c_str(),
-                    LYD_ANYDATA_CONSTSTRING,
-                    0);
+            if (!output) {
+                output = session.getContext().newPath(p.c_str(), v.type() == typeid(empty_) ? nullptr : leafDataToString(v).c_str());
             } else {
-                res->new_path(
-                    sess->get_context(),
-                    p.c_str(),
-                    v.type() == typeid(empty_) ? nullptr : leafDataToString(v).c_str(),
-                    LYD_ANYDATA_CONSTSTRING,
-                    0);
+                output->newPath(p.c_str(), v.type() == typeid(empty_) ? nullptr : leafDataToString(v).c_str());
             }
         }
-        parent = res;
-        return SR_ERR_OK;
+        return sysrepo::ErrorCode::Ok;
     }
 
 private:
@@ -167,9 +110,6 @@
 };
 
 OperationalDataSubscription::OperationalDataSubscription(const std::string& moduleName, const std::string& path, const DataSupplier& dataSupplier)
-    : m_connection(std::make_shared<sysrepo::Connection>())
-    , m_session(std::make_shared<sysrepo::Session>(m_connection))
-    , m_subscription(std::make_shared<sysrepo::Subscribe>(m_session))
+    : m_subscription(sysrepo::Connection{}.sessionStart().onOperGet(moduleName.c_str(), OperationalDataCallback{dataSupplier}, path.c_str()))
 {
-    m_subscription->oper_get_items_subscribe(moduleName.c_str(), OperationalDataCallback{dataSupplier}, path.c_str());
 }
diff --git a/tests/mock/sysrepo_subscription.hpp b/tests/mock/sysrepo_subscription.hpp
index fc2e499..85d048d 100644
--- a/tests/mock/sysrepo_subscription.hpp
+++ b/tests/mock/sysrepo_subscription.hpp
@@ -10,21 +10,15 @@
 
 #include <memory>
 #include <optional>
-#include <sysrepo-cpp/Session.hpp>
+#include <sysrepo-cpp/Connection.hpp>
 #include "datastore_access.hpp"
 
-namespace sysrepo {
-class Callback;
-class Connection;
-class Session;
-class Subscribe;
-}
 class YangSchema;
 
 class Recorder {
 public:
     virtual ~Recorder();
-    virtual void write(const std::string& xpath, const std::optional<std::string>& oldValue, const std::optional<std::string>& newValue) = 0;
+    virtual void write(const sysrepo::ChangeOperation operation, const std::string& xpath, const std::optional<std::string>& oldValue, const std::optional<std::string>& newValue, const std::optional<std::string> previousList) = 0;
 };
 
 class DataSupplier {
@@ -36,13 +30,10 @@
 
 class SysrepoSubscription {
 public:
-    SysrepoSubscription(const std::string& moduleName, Recorder* rec = nullptr, sr_datastore_t ds = SR_DS_RUNNING);
+    SysrepoSubscription(const std::string& moduleName, Recorder* rec = nullptr, sysrepo::Datastore ds = sysrepo::Datastore::Running);
 
 private:
-    std::shared_ptr<sysrepo::Connection> m_connection;
-    std::shared_ptr<sysrepo::Session> m_session;
-    std::shared_ptr<YangSchema> m_schema;
-    std::shared_ptr<sysrepo::Subscribe> m_subscription;
+    sysrepo::Subscription m_subscription;
 };
 
 class OperationalDataSubscription {
@@ -50,8 +41,6 @@
     OperationalDataSubscription(const std::string& moduleName, const std::string& path, const DataSupplier& dataSupplier);
 
 private:
-    std::shared_ptr<sysrepo::Connection> m_connection;
-    std::shared_ptr<sysrepo::Session> m_session;
     std::shared_ptr<YangSchema> m_schema;
-    std::shared_ptr<sysrepo::Subscribe> m_subscription;
+    sysrepo::Subscription m_subscription;
 };
diff --git a/tests/utils.cpp b/tests/utils.cpp
index 187cdf3..c24f4de 100644
--- a/tests/utils.cpp
+++ b/tests/utils.cpp
@@ -7,6 +7,7 @@
 */
 
 #include "trompeloeil_doctest.hpp"
+#include <libyang-cpp/Context.hpp>
 #include "completion.hpp"
 #include "leaf_data_helpers.hpp"
 #include "libyang_utils.hpp"
@@ -206,12 +207,6 @@
         }
     }
 
-    leaf leafRefNonPresent {
-        type leafref {
-            path ../stuff/name;
-        }
-    }
-
     container users {
         config false;
         list userList {
@@ -238,7 +233,7 @@
     "test-schema:enum": "A",
     "test-schema:identityRef": "apple",
     "test-schema:binary": "QUhPSgo=",
-    "test-schema:empty": "",
+    "test-schema:empty": [null],
     "test-schema:bits": "a AHOJ",
     "test-schema:capabilities": "switch hub",
     "test-schema:dec64": "43242.43260",
@@ -248,7 +243,6 @@
         }
     ],
     "test-schema:leafRefPresent": "Xaver",
-    "test-schema:leafRefNonPresent": "Lucas",
     "test-schema:users": {
         "userList": [
             {
@@ -268,9 +262,9 @@
 
 TEST_CASE("libyang_utils")
 {
-    auto ctx = std::make_shared<libyang::Context>();
-    ctx->parse_module_mem(schema, LYS_IN_YANG);
-    auto dataNode = ctx->parse_data_mem(data, LYD_JSON, LYD_OPT_DATA_NO_YANGLIB | LYD_OPT_NOEXTDEPS | LYD_OPT_STRICT);
+    libyang::Context ctx;
+    ctx.parseModuleMem(schema, libyang::SchemaFormat::YANG);
+    auto dataNode = ctx.parseDataMem(data, libyang::DataFormat::JSON, std::nullopt, libyang::ValidationOptions::Present);
 
     SECTION("leafValueFromNode")
     {
@@ -362,16 +356,9 @@
             path = "test-schema:leafRefPresent";
             expectedLeafData = std::string{"Xaver"};
         }
-        SECTION("test-schema:leafRefNonPresent")
-        {
-            path = "test-schema:leafRefNonPresent";
-            expectedLeafData = std::string{"Lucas"};
-        }
 
-        auto leaf = dataNode->find_path(("/" + path).c_str());
-        REQUIRE(leaf->number() == 1);
-        auto firstLeaf = std::make_shared<libyang::Data_Node_Leaf_List>(leaf->data().front());
-        REQUIRE(leafValueFromNode(firstLeaf) == expectedLeafData);
+        auto leaf = dataNode->findPath(("/" + path).c_str());
+        REQUIRE(leafValueFromNode(leaf->asTerm()) == expectedLeafData);
     }
 
     SECTION("lyNodesToTree")
@@ -397,7 +384,6 @@
             {"/test-schema:stuff[name='Xaver']", special_{SpecialValue::List}},
             {"/test-schema:stuff[name='Xaver']/name", std::string{"Xaver"}},
             {"/test-schema:leafRefPresent", std::string{"Xaver"}},
-            {"/test-schema:leafRefNonPresent", std::string{"Lucas"}},
             {"/test-schema:users/userList[1]", special_{SpecialValue::List}},
             {"/test-schema:users/userList[1]/name", std::string{"John"}},
             {"/test-schema:users/userList[2]", special_{SpecialValue::List}},
@@ -407,7 +393,7 @@
         };
 
         DatastoreAccess::Tree tree;
-        lyNodesToTree(tree, {dataNode->tree_for()});
+        lyNodesToTree(tree, dataNode->siblings());
         REQUIRE(tree == expected);
     }
 }
diff --git a/tests/yang.cpp b/tests/yang.cpp
index ed4da88..205616c 100644
--- a/tests/yang.cpp
+++ b/tests/yang.cpp
@@ -683,7 +683,7 @@
                 }
                 SECTION("bigPizzas enabled")
                 {
-                    ys.enableFeature("example-schema", "bigPizzas");
+                    ys.setEnabledFeatures("example-schema", {"bigPizzas"});
                     type = createEnum({"small", "medium", "large"});
                 }
             }
@@ -727,7 +727,7 @@
                 node.first = "example-schema";
                 node.second = "activeNumber";
                 type.emplace<yang::LeafRef>(
-                    "/example-schema:_list/number",
+                    "/_list/number",
                     std::make_unique<yang::TypeInfo>(ys.leafType("/example-schema:_list/number"))
                 );
             }
@@ -744,22 +744,19 @@
                 }
                 SECTION("weird ports enabled")
                 {
-                    ys.enableFeature("example-schema", "weirdPortNames");
+                    ys.setEnabledFeatures("example-schema", {"weirdPortNames"});
                     enums = createEnum({"WEIRD", "utf2", "utf3"});
                 }
 
                 type = yang::Union{{
                     yang::TypeInfo{createEnum({"wlan0", "wlan1"})},
                     yang::TypeInfo{yang::LeafRef{
-                        "/example-schema:portSettings/port",
+                        "../portSettings/port",
                         std::make_unique<yang::TypeInfo>(createEnum({"eth0", "eth1", "eth2"}))
                     }},
                     yang::TypeInfo{yang::LeafRef{
-                        "/example-schema:activeMappedPort",
-                        std::make_unique<yang::TypeInfo>(yang::LeafRef{
-                                "/example-schema:portMapping/port",
-                                std::make_unique<yang::TypeInfo>(enums)
-                        })
+                        "../activeMappedPort",
+                        std::make_unique<yang::TypeInfo>(enums)
                     }},
                     yang::TypeInfo{yang::Empty{}},
                 }};
@@ -939,6 +936,7 @@
                         {"example-schema"s, "zero"},
                         {"example-schema"s, "subLeaf"}
                     };
+
                     expectedRecursive = {
                         {boost::none, "/example-schema:_list"},
                         {boost::none, "/example-schema:_list/contInList"},
@@ -966,10 +964,10 @@
                         {boost::none, "/example-schema:foodDrinkIdentLeaf"},
                         {boost::none, "/example-schema:foodDrinkIdentLeaf"},
                         {boost::none, "/example-schema:foodIdentLeaf"},
-                        {boost::none, "/example-schema:interface/caseEthernet/ethernet"},
-                        {boost::none, "/example-schema:interface/caseEthernet/ethernet/ip"},
-                        {boost::none, "/example-schema:interface/caseLoopback/loopback"},
-                        {boost::none, "/example-schema:interface/caseLoopback/loopback/ip"},
+                        {boost::none, "/example-schema:ethernet"},
+                        {boost::none, "/example-schema:ethernet/ip"},
+                        {boost::none, "/example-schema:loopback"},
+                        {boost::none, "/example-schema:loopback/ip"},
                         {boost::none, "/example-schema:interrupt"},
                         {boost::none, "/example-schema:leafBool"},
                         {boost::none, "/example-schema:leafDecimal"},
@@ -990,16 +988,10 @@
                         {boost::none, "/example-schema:leafUint8"},
                         {boost::none, "/example-schema:length"},
                         {boost::none, "/example-schema:myRpc"},
-                        {boost::none, "/example-schema:myRpc/input"},
-                        {boost::none, "/example-schema:myRpc/output"},
                         {boost::none, "/example-schema:rpcOneOutput"},
-                        {boost::none, "/example-schema:rpcOneOutput/input"},
-                        {boost::none, "/example-schema:rpcOneOutput/output"},
-                        {boost::none, "/example-schema:rpcOneOutput/output/ahoj"},
+                        {boost::none, "/example-schema:rpcOneOutput/ahoj"},
                         {boost::none, "/example-schema:rpcOneInput"},
-                        {boost::none, "/example-schema:rpcOneInput/input"},
-                        {boost::none, "/example-schema:rpcOneInput/input/ahoj"},
-                        {boost::none, "/example-schema:rpcOneInput/output"},
+                        {boost::none, "/example-schema:rpcOneInput/ahoj"},
                         {boost::none, "/example-schema:numberOrString"},
                         {boost::none, "/example-schema:obsoleteLeaf"},
                         {boost::none, "/example-schema:obsoleteLeafWithDeprecatedType"},
@@ -1010,10 +1002,6 @@
                         {boost::none, "/example-schema:portMapping/port"},
                         {boost::none, "/example-schema:portSettings"},
                         {boost::none, "/example-schema:portSettings/port"},
-                        {boost::none, "/example-schema:portSettings/shutdown"},
-                        {boost::none, "/example-schema:portSettings/shutdown/input"},
-                        {boost::none, "/example-schema:portSettings/shutdown/output"},
-                        {boost::none, "/example-schema:portSettings/shutdown/output/success"},
                         {boost::none, "/example-schema:systemStats"},
                         {boost::none, "/example-schema:systemStats/upTime"},
                         {boost::none, "/example-schema:subLeaf"},
@@ -1196,7 +1184,7 @@
 
         SECTION("leafrefPath")
         {
-            REQUIRE(ys.leafrefPath("/example-schema:activeNumber") == "/example-schema:_list/number");
+            REQUIRE(ys.leafrefPath("/example-schema:activeNumber") == "/_list/number");
         }
 
         SECTION("isConfig")
@@ -1217,7 +1205,7 @@
         SECTION("leafTypeName")
         {
             REQUIRE(ys.leafTypeName("/example-schema:leafEnumTypedefRestricted") == "enumTypedef");
-            REQUIRE(ys.leafTypeName("/example-schema:leafInt32") == std::nullopt);
+            REQUIRE(ys.leafTypeName("/example-schema:leafInt32") == "int32");
         }
 
         SECTION("dataPathToSchemaPath")
@@ -1334,14 +1322,14 @@
             REQUIRE_THROWS(ys.nodeType(path, node));
         }
 
-        SECTION("enableFeature - non existing module")
+        SECTION("setEnabledFeatures - non existing module")
         {
-            REQUIRE_THROWS_AS(ys.enableFeature("non-existing", "just-no"), std::runtime_error);
+            REQUIRE_THROWS_AS(ys.setEnabledFeatures("non-existing", {"just-no"}), std::runtime_error);
         }
 
-        SECTION("enableFeature - non existing feature")
+        SECTION("setEnabledFeatures - non existing feature")
         {
-            REQUIRE_THROWS_AS(ys.enableFeature("example-schema", "just-no"), std::runtime_error);
+            REQUIRE_THROWS_AS(ys.setEnabledFeatures("example-schema", {"just-no"}), std::runtime_error);
         }
     }
 }