diff --git a/python/vyos/xml_ref/__init__.py b/python/vyos/xml_ref/__init__.py
index 66525865c..ae5184746 100644
--- a/python/vyos/xml_ref/__init__.py
+++ b/python/vyos/xml_ref/__init__.py
@@ -1,58 +1,66 @@
 # Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public License
 # along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 from vyos.xml_ref import definition
 
 def load_reference(cache=[]):
     if cache:
         return cache[0]
 
     xml = definition.Xml()
 
     try:
         from vyos.xml_ref.cache import reference
         xml.define(reference)
         cache.append(xml)
     except Exception:
         raise ImportError('no xml reference cache !!')
 
     return xml
 
 def is_tag(path: list) -> bool:
     return load_reference().is_tag(path)
 
 def is_tag_value(path: list) -> bool:
     return load_reference().is_tag_value(path)
 
 def is_multi(path: list) -> bool:
     return load_reference().is_multi(path)
 
 def is_valueless(path: list) -> bool:
     return load_reference().is_valueless(path)
 
 def is_leaf(path: list) -> bool:
     return load_reference().is_leaf(path)
 
 def component_version() -> dict:
     return load_reference().component_version()
 
 def multi_to_list(rpath: list, conf: dict) -> dict:
     return load_reference().multi_to_list(rpath, conf)
 
-def get_defaults(path: list, get_first_key=False) -> dict:
-    return load_reference().get_defaults(path, get_first_key=get_first_key)
+def get_defaults(path: list, get_first_key=False, recursive=False) -> dict:
+    return load_reference().get_defaults(path, get_first_key=get_first_key,
+                                         recursive=recursive)
+
+def get_config_defaults(rpath: list, conf: dict, get_first_key=False,
+                        recursive=False) -> dict:
+
+    return load_reference().relative_defaults(rpath, conf=conf,
+                                              get_first_key=get_first_key,
+                                              recursive=recursive)
 
 def merge_defaults(path: list, conf: dict) -> dict:
     return load_reference().merge_defaults(path, conf)
diff --git a/python/vyos/xml_ref/definition.py b/python/vyos/xml_ref/definition.py
index 64a33e4d0..429331577 100644
--- a/python/vyos/xml_ref/definition.py
+++ b/python/vyos/xml_ref/definition.py
@@ -1,234 +1,231 @@
 # Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public License
 # along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
-from typing import Union, Optional, Any
+from typing import Union, Any
 from vyos.configdict import dict_merge
 
 class Xml:
     def __init__(self):
         self.ref = {}
 
     def define(self, ref: dict):
         self.ref = ref
 
     def _get_ref_node_data(self, node: dict, data: str) -> Union[bool, str]:
         res = node.get('node_data', {})
         if not res:
             raise ValueError("non-existent node data")
         if data not in res:
             raise ValueError("non-existent data field")
 
         return res.get(data)
 
     def _get_ref_path(self, path: list) -> dict:
         ref_path = path.copy()
         d = self.ref
         while ref_path and d:
             d = d.get(ref_path[0], {})
             ref_path.pop(0)
             if self._is_tag_node(d) and ref_path:
                 ref_path.pop(0)
 
         return d
 
     def _is_tag_node(self, node: dict) -> bool:
         res = self._get_ref_node_data(node, 'node_type')
         return res == 'tag'
 
     def is_tag(self, path: list) -> bool:
         ref_path = path.copy()
         d = self.ref
         while ref_path and d:
             d = d.get(ref_path[0], {})
             ref_path.pop(0)
             if self._is_tag_node(d) and ref_path:
                 if len(ref_path) == 1:
                     return False
                 ref_path.pop(0)
 
         return self._is_tag_node(d)
 
     def is_tag_value(self, path: list) -> bool:
         if len(path) < 2:
             return False
 
         return self.is_tag(path[:-1])
 
     def _is_multi_node(self, node: dict) -> bool:
         b = self._get_ref_node_data(node, 'multi')
         assert isinstance(b, bool)
         return b
 
     def is_multi(self, path: list) -> bool:
         d = self._get_ref_path(path)
         return  self._is_multi_node(d)
 
     def _is_valueless_node(self, node: dict) -> bool:
         b = self._get_ref_node_data(node, 'valueless')
         assert isinstance(b, bool)
         return b
 
     def is_valueless(self, path: list) -> bool:
         d = self._get_ref_path(path)
         return  self._is_valueless_node(d)
 
     def _is_leaf_node(self, node: dict) -> bool:
         res = self._get_ref_node_data(node, 'node_type')
         return res == 'leaf'
 
     def is_leaf(self, path: list) -> bool:
         d = self._get_ref_path(path)
         return self._is_leaf_node(d)
 
     def component_version(self) -> dict:
         d = {}
         for k, v in self.ref['component_version']:
             d[k] = int(v)
         return d
 
     def multi_to_list(self, rpath: list, conf: dict) -> dict:
         if rpath and rpath[-1] in list(conf):
             raise ValueError('rpath should be disjoint from conf keys')
 
-        res = {}
+        res: Any = {}
 
         for k in list(conf):
             d = self._get_ref_path(rpath + [k])
             if self._is_leaf_node(d):
                 if self._is_multi_node(d) and not isinstance(conf[k], list):
                     res[k] = [conf[k]]
                 else:
                     res[k] = conf[k]
             else:
                 res[k] = self.multi_to_list(rpath + [k], conf[k])
 
         return res
 
     def _get_default_value(self, node: dict):
         return self._get_ref_node_data(node, "default_value")
 
-    def get_defaults(self, path: list, get_first_key=False) -> dict:
+    def get_defaults(self, path: list, get_first_key=False, recursive=False) -> dict:
         """Return dict containing default values below path
 
         Note that descent below path will not proceed beyond an encountered
         tag node, as no tag node value is known. For a default dict relative
         to an existing config dict containing tag node values, see function:
         'relative_defaults'
         """
-        res: Any = {}
+        res: dict = {}
         d = self._get_ref_path(path)
-        if self._is_leaf_node(d):
-            default_value = self._get_default_value(d)
-            if default_value is not None:
-                res = default_value
-                if self._is_multi_node(d) and not isinstance(res, list):
-                    res = [res]
-        elif self.is_tag(path):
-            # tag node defaults are used as suggestion, not default value;
-            # should this change, append to path and continue recursion
-            pass
-        else:
-            for k in list(d):
-                if k in ('node_data', 'component_version') :
-                    continue
-                pos = self.get_defaults(path + [k])
-                res |= pos
+        for k in list(d):
+            if k in ('node_data', 'component_version') :
+                continue
+            d_k = d[k]
+            if self._is_leaf_node(d_k):
+                default_value = self._get_default_value(d_k)
+                if default_value is not None:
+                    pos = default_value
+                    if self._is_multi_node(d_k) and not isinstance(pos, list):
+                        pos = [pos]
+                    res |= {k: pos}
+            elif self.is_tag(path + [k]):
+                # tag node defaults are used as suggestion, not default value;
+                # should this change, append to path and continue if recursive
+                pass
+            else:
+                if recursive:
+                    pos = self.get_defaults(path + [k], recursive=True)
+                    res |= pos
         if res:
             if get_first_key or not path:
                 if not isinstance(res, dict):
                     raise TypeError("Cannot get_first_key as data under node is not of type dict")
                 return res
             return {path[-1]: res}
 
         return {}
 
-    def _relative_defaults(self, rpath: list, conf: Optional[dict] = None,
-                          get_first_key=False) -> dict:
-        # Return dict containing defaults along paths of a config dict
-
-        # Note that for conf argument {} or None, this function returns the
-        # same result as 'get_defaults' above; for clarity, the functions
-        # are individually defined.
-
-        if conf is None:
-            conf = {}
-        if rpath and rpath[-1] in list(conf):
-            raise ValueError('rpath should be disjoint from conf keys')
-        res: Any = {}
-        d = self._get_ref_path(rpath)
-        if self._is_leaf_node(d):
-            default_value = self._get_default_value(d)
-            if default_value is not None:
-                res = default_value
-                if self._is_multi_node(d) and not isinstance(res, list):
-                    res = [res]
-        elif self.is_tag(rpath):
-            for k in list(conf):
-                pos = self._relative_defaults(rpath + [k], conf[k])
-                res |= pos
-        else:
-            for k in list(d):
-                if k in ('node_data', 'component_version') :
-                    continue
-                pos = self._relative_defaults(rpath + [k], conf.get(k, {}))
-                res |= pos
-        if res:
-            if get_first_key or not rpath:
-                if not isinstance(res, dict):
-                    raise TypeError("Cannot get_first_key as data under node is not of type dict")
-                return res
-            return {rpath[-1]: res}
-
-        return {}
-
-    def _defines_config_path(self, path: list, conf: dict) -> bool:
+    def _well_defined(self, path: list, conf: dict) -> bool:
         # test disjoint path + conf for sensible config paths
-        def walk(c):
+        def step(c):
             return [next(iter(c.keys()))] if c else []
         try:
-            tmp = walk(conf)
+            tmp = step(conf)
             if self.is_tag_value(path + tmp):
                 c = conf[tmp[0]]
                 if not isinstance(c, dict):
                     raise ValueError
-                tmp = tmp + walk(c)
+                tmp = tmp + step(c)
                 self._get_ref_path(path + tmp)
             else:
                 self._get_ref_path(path + tmp)
         except ValueError:
             return False
         return True
 
+    def relative_defaults(self, rpath: list, conf: dict, get_first_key=False,
+                          recursive=False) -> dict:
+        """Return dict containing defaults along paths of a config dict
+        """
+        if not conf:
+            return self.get_defaults(rpath, get_first_key=get_first_key,
+                                     recursive=recursive)
+        if rpath and rpath[-1] in list(conf):
+            conf = conf[rpath[-1]]
+            if not isinstance(conf, dict):
+                raise TypeError('conf at path is not of type dict')
+
+        if not self._well_defined(rpath, conf):
+            print('path to config dict does not define full config paths')
+            return {}
+
+        res: dict = {}
+        for k in list(conf):
+            pos = self.get_defaults(rpath + [k], recursive=recursive)
+            res |= pos
+
+            if isinstance(conf[k], dict):
+                step = self.relative_defaults(rpath + [k], conf=conf[k],
+                                              recursive=recursive)
+                res |= step
+
+        if res:
+            if get_first_key:
+                return res
+            return {rpath[-1]: res} if rpath else res
+
+        return {}
+
     def merge_defaults(self, path: list, conf: dict) -> dict:
         """Return config dict with defaults non-destructively merged
+
+        This merges non-recursive defaults relative to the config dict.
         """
-        if not path:
-            path = [next(iter(conf.keys()))]
         if path[-1] in list(conf):
             config = conf[path[-1]]
             if not isinstance(config, dict):
-                raise ValueError('conf at path is not of type dict')
-            first = False
+                raise TypeError('conf at path is not of type dict')
+            shift = False
         else:
             config = conf
-            first = True
+            shift = True
 
-        if not self._defines_config_path(path, config):
-            print('path + conf do not define config paths; conf returned unchanged')
+        if not self._well_defined(path, config):
+            print('path to config dict does not define config paths; conf returned unchanged')
             return conf
 
-        d = self._relative_defaults(path, conf=config, get_first_key=first)
+        d = self.relative_defaults(path, conf=config, get_first_key=shift)
         d = dict_merge(d, conf)
         return d