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/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;
 };