diff --git a/qapi/misc.json b/qapi/misc.json
index 460866cf542f..d7b776a5af37 100644
--- a/qapi/misc.json
+++ b/qapi/misc.json
@@ -348,52 +348,81 @@
 #
 # @s390: since 2.12
 #
 # @riscv: since 2.12
 #
 # Since: 2.6
 ##
 { 'enum': 'CpuInfoArch',
   'data': ['x86', 'sparc', 'ppc', 'mips', 'tricore', 's390', 'riscv', 'other' ] }
 
 ##
-# @CpuInfo:
+# @CpuInfoCommon:
 #
-# Information about a virtual CPU
+# Collects fields common to @CpuInfoBase and @CpuInfoFastBase; that is,
+# fields that are shared by @query-cpus and @query-cpus-fast, and not
+# specific to the target architecture.
+#
+# @qom-path: path to the CPU object in the QOM tree (since 2.4)
+#
+# @thread-id: ID of the underlying host thread
+#
+# @props: properties describing which node/socket/core/thread the
+#         virtual CPU belongs to, if supported by the board (since 2.10)
+#
+# Since: 2.13
+##
+{ 'struct' : 'CpuInfoCommon',
+  'data'   : { 'qom-path'  : 'str',
+               'thread-id' : 'int',
+               '*props'    : 'CpuInstanceProperties' } }
+
+##
+# @CpuInfoBase:
+#
+# Extends @CpuInfoCommon with fields that are specific to the @query-cpus
+# command, but not specific to the target architecture.
 #
 # @CPU: the index of the virtual CPU
 #
 # @current: this only exists for backwards compatibility and should be ignored
 #
 # @halted: true if the virtual CPU is in the halt state.  Halt usually refers
 #          to a processor specific low power mode.
 #
-# @qom_path: path to the CPU object in the QOM tree (since 2.4)
-#
-# @thread_id: ID of the underlying host thread
-#
-# @props: properties describing to which node/socket/core/thread
-#         virtual CPU belongs to, provided if supported by board (since 2.10)
-#
 # @arch: architecture of the cpu, which determines which additional fields
 #        will be listed (since 2.6)
 #
-# Since: 0.14.0
+# Since: 2.13
 #
 # Notes: @halted is a transient state that changes frequently.  By the time the
 #        data is sent to the client, the guest may no longer be halted.
+#        Moreover, @arch cannot be moved up to @CpuInfoCommon because
+#        that would prevent its use as the discriminator in @CpuInfo.
+##
+{ 'struct' : 'CpuInfoBase',
+  'base'   : 'CpuInfoCommon',
+  'data'   : { 'CPU'     : 'int',
+               'current' : 'bool',
+               'halted'  : 'bool',
+               'arch'    : 'CpuInfoArch' } }
+
+##
+# @CpuInfo:
+#
+# Information about a virtual CPU
+#
+# Since: 0.14.0
 ##
 { 'union': 'CpuInfo',
-  'base': {'CPU': 'int', 'current': 'bool', 'halted': 'bool',
-           'qom_path': 'str', 'thread_id': 'int',
-           '*props': 'CpuInstanceProperties', 'arch': 'CpuInfoArch' },
+  'base': 'CpuInfoBase',
   'discriminator': 'arch',
   'data': { 'x86': 'CpuInfoX86',
             'sparc': 'CpuInfoSPARC',
             'ppc': 'CpuInfoPPC',
             'mips': 'CpuInfoMIPS',
             'tricore': 'CpuInfoTricore',
             's390': 'CpuInfoS390',
             'riscv': 'CpuInfoRISCV',
             'other': 'CpuInfoOther' } }
 
 ##
@@ -512,70 +541,77 @@
 #
 # Since: 0.14.0
 #
 # Example:
 #
 # -> { "execute": "query-cpus" }
 # <- { "return": [
 #          {
 #             "CPU":0,
 #             "current":true,
 #             "halted":false,
-#             "qom_path":"/machine/unattached/device[0]",
+#             "qom-path":"/machine/unattached/device[0]",
 #             "arch":"x86",
 #             "pc":3227107138,
-#             "thread_id":3134
+#             "thread-id":3134
 #          },
 #          {
 #             "CPU":1,
 #             "current":false,
 #             "halted":true,
-#             "qom_path":"/machine/unattached/device[2]",
+#             "qom-path":"/machine/unattached/device[2]",
 #             "arch":"x86",
 #             "pc":7108165,
-#             "thread_id":3135
+#             "thread-id":3135
 #          }
 #       ]
 #    }
 #
 # Notes: This interface is deprecated (since 2.12.0), and it is strongly
 #        recommended that you avoid using it. Use @query-cpus-fast to
 #        obtain information about virtual CPUs.
 #
 ##
 { 'command': 'query-cpus', 'returns': ['CpuInfo'] }
 
 ##
-# @CpuInfoFast:
+# @CpuInfoFastBase:
 #
-# Information about a virtual CPU
+# Extends @CpuInfoCommon with fields that are specific to the
+# @query-cpus-fast command, but not specific to the target architecture.
 #
 # @cpu-index: index of the virtual CPU
 #
-# @qom-path: path to the CPU object in the QOM tree
-#
-# @thread-id: ID of the underlying host thread
-#
-# @props: properties describing to which node/socket/core/thread
-#         virtual CPU belongs to, provided if supported by board
-#
 # @arch: architecture of the cpu, which determines which additional fields
 #        will be listed
 #
+# Since: 2.13
+#
+# Notes: @arch cannot be moved up to @CpuInfoCommon because that would
+#        prevent its use as the discriminator in @CpuInfoFast.
+##
+{ 'struct' : 'CpuInfoFastBase',
+  'base'   : 'CpuInfoCommon',
+  'data'   : { 'cpu-index' : 'int',
+               'arch'      : 'CpuInfoArch' } }
+
+##
+# @CpuInfoFast:
+#
+# Information about a virtual CPU
+#
 # Since: 2.12
 #
 ##
 { 'union': 'CpuInfoFast',
-  'base': {'cpu-index': 'int', 'qom-path': 'str',
-           'thread-id': 'int', '*props': 'CpuInstanceProperties',
-           'arch': 'CpuInfoArch' },
+  'base': 'CpuInfoFastBase',
   'discriminator': 'arch',
   'data': { 'x86': 'CpuInfoOther',
             'sparc': 'CpuInfoOther',
             'ppc': 'CpuInfoOther',
             'mips': 'CpuInfoOther',
             'tricore': 'CpuInfoOther',
             's390': 'CpuInfoS390',
             'riscv': 'CpuInfoOther',
             'other': 'CpuInfoOther' } }
 
 ##
diff --git a/qapi/qapi-schema.json b/qapi/qapi-schema.json
index 25bce78352b8..5bfd2ef1dd3b 100644
--- a/qapi/qapi-schema.json
+++ b/qapi/qapi-schema.json
@@ -61,23 +61,23 @@
         'query-migrate-cache-size',
         'query-tpm-models',
         'query-tpm-types',
         'ringbuf-read' ],
     'name-case-whitelist': [
         'ACPISlotType',         # DIMM, visible through query-acpi-ospm-status
         'CpuInfoMIPS',          # PC, visible through query-cpu
         'CpuInfoTricore',       # PC, visible through query-cpu
         'QapiErrorClass',       # all members, visible through errors
         'UuidInfo',             # UUID, visible through query-uuid
         'X86CPURegister32',     # all members, visible indirectly through qom-get
-        'q_obj_CpuInfo-base'    # CPU, visible through query-cpu
+        'CpuInfoBase'           # CPU, visible through query-cpu
     ] } }
 
 # Documentation generated with qapi-gen.py is in source order, with
 # included sub-schemas inserted at the first include directive
 # (subsequent include directives have no effect).  To get a sane and
 # stable order, it's best to include each sub-schema just once, or
 # include it first right here.
 
 { 'include': 'common.json' }
 { 'include': 'sockets.json' }
 { 'include': 'run-state.json' }
diff --git a/tests/test-x86-cpuid-compat.c b/tests/test-x86-cpuid-compat.c
index 02e41843fc9c..df51b3bbacbc 100644
--- a/tests/test-x86-cpuid-compat.c
+++ b/tests/test-x86-cpuid-compat.c
@@ -10,23 +10,23 @@ static char *get_cpu0_qom_path(void)
 {
     QDict *resp;
     QList *ret;
     QDict *cpu0;
     char *path;
 
     resp = qmp("{'execute': 'query-cpus', 'arguments': {}}");
     g_assert(qdict_haskey(resp, "return"));
     ret = qdict_get_qlist(resp, "return");
 
     cpu0 = qobject_to(QDict, qlist_peek(ret));
-    path = g_strdup(qdict_get_str(cpu0, "qom_path"));
+    path = g_strdup(qdict_get_str(cpu0, "qom-path"));
     QDECREF(resp);
     return path;
 }
 
 static QObject *qom_get(const char *path, const char *prop)
 {
     QDict *resp = qmp("{ 'execute': 'qom-get',"
                       "  'arguments': { 'path': %s,"
                       "                 'property': %s } }",
                       path, prop);
     QObject *ret = qdict_get(resp, "return");
diff --git a/tests/migration/guestperf/engine.py b/tests/migration/guestperf/engine.py
index e14d4320b239..663881c163e9 100644
--- a/tests/migration/guestperf/engine.py
+++ b/tests/migration/guestperf/engine.py
@@ -104,23 +104,23 @@ class Engine(object):
             info.get("x-cpu-throttle-percentage", 0),
         )
 
     def _migrate(self, hardware, scenario, src, dst, connect_uri):
         src_qemu_time = []
         src_vcpu_time = []
         src_pid = src.get_pid()
 
         vcpus = src.command("query-cpus")
         src_threads = []
         for vcpu in vcpus:
-            src_threads.append(vcpu["thread_id"])
+            src_threads.append(vcpu["thread-id"])
 
         # XXX how to get dst timings on remote host ?
 
         if self._verbose:
             print "Sleeping %d seconds for initial guest workload run" % self._sleep
         sleep_secs = self._sleep
         while sleep_secs > 1:
             src_qemu_time.append(self._cpu_timing(src_pid))
             src_vcpu_time.extend(self._vcpu_timing(src_pid, src_threads))
             time.sleep(1)
             sleep_secs -= 1
