diff --git a/debian/control b/debian/control
index cf766a825..d7cd5b688 100644
--- a/debian/control
+++ b/debian/control
@@ -1,200 +1,201 @@
 Source: vyos-1x
 Section: contrib/net
 Priority: extra
 Maintainer: VyOS Package Maintainers <maintainers@vyos.net>
 Build-Depends:
   debhelper (>= 9),
   dh-python,
   fakeroot,
   gcc-multilib [amd64],
   clang [amd64],
   llvm [amd64],
   libbpf-dev [amd64],
   libelf-dev (>= 0.2) [amd64],
   libpcap-dev [amd64],
   build-essential,
   libvyosconfig0 (>= 0.0.7),
   libzmq3-dev,
   python3,
   python3-coverage,
   python3-lxml,
   python3-netifaces,
   python3-nose,
   python3-jinja2,
   python3-psutil,
   python3-setuptools,
   python3-sphinx,
   python3-xmltodict,
+  python3-pyhumps,
   quilt,
   whois
 Standards-Version: 3.9.6
 
 Package: vyos-1x
 Architecture: amd64 arm64
 Depends:
   ${python3:Depends},
   accel-ppp,
   avahi-daemon,
   beep,
   bmon,
   bsdmainutils,
   conntrack,
   conntrackd,
   conserver-client,
   conserver-server,
   console-data,
   crda,
   cron,
   curl,
   dbus,
   ddclient (>= 3.9.1),
   dropbear,
   easy-rsa,
   etherwake,
   ethtool,
   fdisk,
   fastnetmon [amd64],
   file,
   frr (>= 7.5),
   frr-pythontools,
   frr-rpki-rtrlib,
   frr-snmp,
   libpam-google-authenticator,
   grc,
   hostapd,
   hvinfo,
   igmpproxy,
   ipaddrcheck,
   iperf,
   iperf3,
   iproute2,
   iputils-arping,
   isc-dhcp-client,
   isc-dhcp-relay,
   isc-dhcp-server,
   iw,
   keepalived (>=2.0.5),
   lcdproc,
   lcdproc-extra-drivers,
   libatomic1,
   libbpf0 [amd64],
   libcharon-extra-plugins (>=5.9),
   libcharon-extauth-plugins (>=5.9),
   libndp-tools,
   libnetfilter-conntrack3,
   libnfnetlink0,
   libpam-radius-auth (>= 1.5.0),
   libqmi-utils,
   libstrongswan-extra-plugins (>=5.9),
   libstrongswan-standard-plugins (>=5.9),
   libvyosconfig0,
   lldpd,
   lm-sensors,
   lsscsi,
   minisign,
   modemmanager,
   mtr-tiny,
   ndisc6,
   ndppd,
   netplug,
   nfct,
   nftables (>= 0.9.3),
   nginx-light,
   ntp,
   ntpdate,
   nvme-cli,
   ocserv,
   opennhrp,
   openssh-server,
   openssl,
   openvpn,
   openvpn-auth-ldap,
   openvpn-auth-radius,
   openvpn-otp,
   owamp-client,
   owamp-server,
   pciutils,
   pdns-recursor,
   pmacct (>= 1.6.0),
   podman,
   pppoe,
   procps,
   python3,
   python3-certbot-nginx,
   python3-cryptography,
   python3-hurry.filesize,
   python3-inotify,
   python3-isc-dhcp-leases,
   python3-jinja2,
   python3-jmespath,
   python3-netaddr,
   python3-netifaces,
   python3-paramiko,
   python3-psutil,
   python3-pyhumps,
   python3-pystache,
   python3-pyudev,
   python3-six,
   python3-tabulate,
   python3-vici (>= 5.7.2),
   python3-voluptuous,
   python3-xmltodict,
   python3-zmq,
   qrencode,
   radvd,
   salt-minion,
   sed,
   smartmontools,
   snmp,
   snmpd,
   squashfs-tools,
   squid,
   squidclient,
   squidguard,
   sshguard,
   ssl-cert,
   strongswan (>= 5.9),
   strongswan-swanctl (>= 5.9),
   stunnel4,
   sudo,
   systemd,
   telegraf (>= 1.20),
   tcpdump,
   tcptraceroute,
   telnet,
   tftpd-hpa,
   traceroute,
   tuned,
   twamp-client,
   twamp-server,
   udp-broadcast-relay,
   uidmap,
   usb-modeswitch,
   usbutils,
   vyatta-bash,
   vyatta-cfg,
   vyos-http-api-tools,
   vyos-utils,
   wide-dhcpv6-client,
   wireguard-tools,
   wireless-regdb,
   wpasupplicant (>= 0.6.7),
   ndppd,
   miniupnpd-nftables
 Description: VyOS configuration scripts and data
  VyOS configuration scripts, interface definitions, and everything
 
 Package: vyos-1x-vmware
 Architecture: amd64
 Depends:
  vyos-1x,
  open-vm-tools
 Description: VyOS configuration scripts and data for VMware
  Adds configuration files required for VyOS running on VMware hosts.
 
 Package: vyos-1x-smoketest
 Architecture: all
 Depends:
  snmp,
  vyos-1x
 Description: VyOS build sanity checking toolkit
diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py
index 727e118a8..2e896c8e6 100644
--- a/python/vyos/opmode.py
+++ b/python/vyos/opmode.py
@@ -1,210 +1,212 @@
 # Copyright 2022 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/>.
 
 import re
 import sys
 import typing
+from humps import decamelize
 
 
 class Error(Exception):
     """ Any error that makes requested operation impossible to complete
         for reasons unrelated to the user input or script logic.
     """
     pass
 
 class UnconfiguredSubsystem(Error):
     """ Requested operation is valid, but cannot be completed
         because corresponding subsystem is not configured and running.
     """
     pass
 
 class DataUnavailable(Error):
     """ Requested operation is valid, but cannot be completed
         because data for it is not available.
         This error MAY be treated as temporary because such issues
         are often caused by transient events such as service restarts.
     """
     pass
 
 class PermissionDenied(Error):
     """ Requested operation is valid, but the caller has no permission
         to perform it.
     """
     pass
 
 class InternalError(Error):
     """ Any situation when VyOS detects that it could not perform
         an operation correctly due to logic errors in its own code
         or errors in underlying software.
     """
     pass
 
 
 def _is_op_mode_function_name(name):
     if re.match(r"^(show|clear|reset|restart)", name):
         return True
     else:
         return False
 
 def _is_show(name):
     if re.match(r"^show", name):
         return True
     else:
         return False
 
 def _get_op_mode_functions(module):
     from inspect import getmembers, isfunction
 
     # Get all functions in that module
     funcs = getmembers(module, isfunction)
 
     # getmembers returns (name, func) tuples
     funcs = list(filter(lambda ft: _is_op_mode_function_name(ft[0]), funcs))
 
     funcs_dict = {}
     for (name, thunk) in funcs:
         funcs_dict[name] = thunk
 
     return funcs_dict
 
 def _is_optional_type(t):
     # Optional[t] is internally an alias for Union[t, NoneType]
     # and there's no easy way to get union members it seems
     if (type(t) == typing._UnionGenericAlias):
         if (len(t.__args__) == 2):
             if t.__args__[1] == type(None):
                 return True
 
     return False
 
 def _get_arg_type(t):
     """ Returns the type itself if it's a primitive type,
         or the "real" type of typing.Optional
 
        Doesn't work with anything else at the moment!
     """
     if _is_optional_type(t):
         return t.__args__[0]
     else:
         return t
 
 def _normalize_field_name(name):
     # Convert the name to string if it is not
     # (in some cases they may be numbers)
     name = str(name)
 
     # Replace all separators with underscores
     name = re.sub(r'(\s|[\(\)\[\]\{\}\-\.\,:\"\'\`])+', '_', name)
 
     # Replace specific characters with textual descriptions
     name = re.sub(r'@', '_at_', name)
     name = re.sub(r'%', '_percentage_', name)
     name = re.sub(r'~', '_tilde_', name)
 
     # Force all letters to lowercase
     name = name.lower()
 
     # Remove leading and trailing underscores, if any
     name = re.sub(r'(^(_+)(?=[^_])|_+$)', '', name)
 
     # Ensure there are only single underscores
     name = re.sub(r'_+', '_', name)
 
     return name
 
 def _normalize_dict_field_names(old_dict):
     new_dict = {}
 
     for key in old_dict:
         new_key = _normalize_field_name(key)
         new_dict[new_key] = _normalize_field_names(old_dict[key])
 
     # Sanity check
     if len(old_dict) != len(new_dict):
         raise InternalError("Dictionary fields do not allow unique normalization")
     else:
         return new_dict
 
 def _normalize_field_names(value):
     if isinstance(value, dict):
         return _normalize_dict_field_names(value)
     elif isinstance(value, list):
         return list(map(lambda v: _normalize_field_names(v), value))
     else:
         return value
 
 def run(module):
     from argparse import ArgumentParser
 
     functions = _get_op_mode_functions(module)
 
     parser = ArgumentParser()
     subparsers = parser.add_subparsers(dest="subcommand")
 
     for function_name in functions:
         subparser = subparsers.add_parser(function_name, help=functions[function_name].__doc__)
 
         type_hints = typing.get_type_hints(functions[function_name])
         if 'return' in type_hints:
             del type_hints['return']
         for opt in type_hints:
             th = type_hints[opt]
 
             if _get_arg_type(th) == bool:
                 subparser.add_argument(f"--{opt}", action='store_true')
             else:
                 if _is_optional_type(th):
                     subparser.add_argument(f"--{opt}", type=_get_arg_type(th), default=None)
                 else:
                     subparser.add_argument(f"--{opt}", type=_get_arg_type(th), required=True)
 
     # Get options as a dict rather than a namespace,
     # so that we can modify it and pack for passing to functions
     args = vars(parser.parse_args())
 
     if not args["subcommand"]:
         print("Subcommand required!")
         parser.print_usage()
         sys.exit(1)
 
     function_name = args["subcommand"]
     func = functions[function_name]
 
     # Remove the subcommand from the arguments,
     # it would cause an extra argument error when we pass the dict to a function
     del args["subcommand"]
 
     # Show commands must always get the "raw" argument,
     # but other commands (clear/reset/restart) should not,
     # because they produce no output and it makes no sense for them.
     if ("raw" not in args) and _is_show(function_name):
         args["raw"] = False
 
     if re.match(r"^show", function_name):
         # Show commands are slightly special:
         # they may return human-formatted output
         # or a raw dict that we need to serialize in JSON for printing
         res = func(**args)
         if not args["raw"]:
             return res
         else:
+            res = decamelize(res)
             res = _normalize_field_names(res)
             from json import dumps
             return dumps(res, indent=4)
     else:
         # Other functions should not return anything,
         # although they may print their own warnings or status messages
         func(**args)
 
diff --git a/src/services/api/graphql/libs/op_mode.py b/src/services/api/graphql/libs/op_mode.py
index da2bcdb5b..97a26520e 100644
--- a/src/services/api/graphql/libs/op_mode.py
+++ b/src/services/api/graphql/libs/op_mode.py
@@ -1,100 +1,106 @@
 # Copyright 2022 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/>.
 
 import os
 import re
 import typing
 import importlib.util
+from typing import Union
+from humps import decamelize
 
 from vyos.defaults import directories
+from vyos.opmode import _normalize_field_names
 
 def load_as_module(name: str, path: str):
     spec = importlib.util.spec_from_file_location(name, path)
     mod = importlib.util.module_from_spec(spec)
     spec.loader.exec_module(mod)
     return mod
 
 def load_op_mode_as_module(name: str):
     path = os.path.join(directories['op_mode'], name)
     name = os.path.splitext(name)[0].replace('-', '_')
     return load_as_module(name, path)
 
 def is_op_mode_function_name(name):
     if re.match(r"^(show|clear|reset|restart)", name):
         return True
     return False
 
 def is_show_function_name(name):
     if re.match(r"^show", name):
         return True
     return False
 
 def _nth_split(delim: str, n: int, s: str):
     groups = s.split(delim)
     l = len(groups)
     if n > l-1 or n < 1:
         return (s, '')
     return (delim.join(groups[:n]), delim.join(groups[n:]))
 
 def _nth_rsplit(delim: str, n: int, s: str):
     groups = s.split(delim)
     l = len(groups)
     if n > l-1 or n < 1:
         return (s, '')
     return (delim.join(groups[:l-n]), delim.join(groups[l-n:]))
 
 # Since we have mangled possible hyphens in the file name while constructing
 # the snake case of the query/mutation name, we will need to recover the
 # file name by searching with mangling:
 def _filter_on_mangled(test):
     def func(elem):
         mangle = os.path.splitext(elem)[0].replace('-', '_')
         return test == mangle
     return func
 
 # Find longest name in concatenated string that matches the basename of an
 # op-mode script. Should one prefer to concatenate in the reverse order
 # (script_name + '_' + function_name), use _nth_rsplit.
 def split_compound_op_mode_name(name: str, files: list):
     for i in range(1, name.count('_') + 1):
         pair = _nth_split('_', i, name)
         f = list(filter(_filter_on_mangled(pair[1]), files))
         if f:
             pair = (pair[0], f[0])
             return pair
     return (name, '')
 
 def snake_to_pascal_case(name: str) -> str:
     res = ''.join(map(str.title, name.split('_')))
     return res
 
 def map_type_name(type_name: type, optional: bool = False) -> str:
     if type_name == str:
         return 'String!' if not optional else 'String = null'
     if type_name == int:
         return 'Int!' if not optional else 'Int = null'
     if type_name == bool:
         return 'Boolean!' if not optional else 'Boolean = false'
     if typing.get_origin(type_name) == list:
         if not optional:
             return f'[{map_type_name(typing.get_args(type_name)[0])}]!'
         return f'[{map_type_name(typing.get_args(type_name)[0])}]'
     # typing.Optional is typing.Union[_, NoneType]
     if (typing.get_origin(type_name) is typing.Union and
             typing.get_args(type_name)[1] == type(None)):
         return f'{map_type_name(typing.get_args(type_name)[0], optional=True)}'
 
     # scalar 'Generic' is defined in schema.graphql
     return 'Generic'
+
+def normalize_output(result: Union[dict, list]) -> Union[dict, list]:
+    return _normalize_field_names(decamelize(result))
diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py
index c2c1db1df..0b77b1433 100644
--- a/src/services/api/graphql/session/session.py
+++ b/src/services/api/graphql/session/session.py
@@ -1,174 +1,177 @@
 # Copyright 2021-2022 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/>.
 
 import os
 import json
 
 from ariadne import convert_camel_case_to_snake
 
 from vyos.config import Config
 from vyos.configtree import ConfigTree
 from vyos.defaults import directories
 from vyos.template import render
 from vyos.opmode import Error as OpModeError
 
 from api.graphql.libs.op_mode import load_op_mode_as_module, split_compound_op_mode_name
+from api.graphql.libs.op_mode import normalize_output
 
 op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json')
 
 class Session:
     """
     Wrapper for calling configsession functions based on GraphQL requests.
     Non-nullable fields in the respective schema allow avoiding a key check
     in 'data'.
     """
     def __init__(self, session, data):
         self._session = session
         self._data = data
         self._name = convert_camel_case_to_snake(type(self).__name__)
 
         try:
             with open(op_mode_include_file) as f:
                 self._op_mode_list = json.loads(f.read())
         except Exception:
             self._op_mode_list = None
 
     def show_config(self):
         session = self._session
         data = self._data
         out = ''
 
         try:
             out = session.show_config(data['path'])
             if data.get('config_format', '') == 'json':
                 config_tree = ConfigTree(out)
                 out = json.loads(config_tree.to_json())
         except Exception as error:
             raise error
 
         return out
 
     def save_config_file(self):
         session = self._session
         data = self._data
         if 'file_name' not in data or not data['file_name']:
             data['file_name'] = '/config/config.boot'
 
         try:
             session.save_config(data['file_name'])
         except Exception as error:
             raise error
 
     def load_config_file(self):
         session = self._session
         data = self._data
 
         try:
             session.load_config(data['file_name'])
             session.commit()
         except Exception as error:
             raise error
 
     def show(self):
         session = self._session
         data = self._data
         out = ''
 
         try:
             out = session.show(data['path'])
         except Exception as error:
             raise error
 
         return out
 
     def add_system_image(self):
         session = self._session
         data = self._data
 
         try:
             res = session.install_image(data['location'])
         except Exception as error:
             raise error
 
         return res
 
     def delete_system_image(self):
         session = self._session
         data = self._data
 
         try:
             res = session.remove_image(data['name'])
         except Exception as error:
             raise error
 
         return res
 
     def system_status(self):
         import api.graphql.session.composite.system_status as system_status
 
         session = self._session
         data = self._data
 
         status = {}
         status['host_name'] = session.show(['host', 'name']).strip()
         status['version'] = system_status.get_system_version()
         status['uptime'] = system_status.get_system_uptime()
         status['ram'] = system_status.get_system_ram_usage()
 
         return status
 
     def gen_op_query(self):
         session = self._session
         data = self._data
         name = self._name
         op_mode_list = self._op_mode_list
 
         # handle the case that the op-mode file contains underscores:
         if op_mode_list is None:
             raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'")
         (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list)
         if scriptname == '':
             raise FileNotFoundError(f"No op-mode file named in string '{name}'")
 
         mod = load_op_mode_as_module(f'{scriptname}')
         func = getattr(mod, func_name)
         try:
             res = func(True, **data)
         except OpModeError as e:
             raise e
 
+        res = normalize_output(res)
+
         return res
 
     def gen_op_mutation(self):
         session = self._session
         data = self._data
         name = self._name
         op_mode_list = self._op_mode_list
 
         # handle the case that the op-mode file name contains underscores:
         if op_mode_list is None:
             raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'")
         (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list)
         if scriptname == '':
             raise FileNotFoundError(f"No op-mode file named in string '{name}'")
 
         mod = load_op_mode_as_module(f'{scriptname}')
         func = getattr(mod, func_name)
         try:
             res = func(**data)
         except OpModeError as e:
             raise e
 
         return res