diff --git a/qapi/char.json b/qapi/char.json
index 43486d1daa..57ec18220b 100644
--- a/qapi/char.json
+++ b/qapi/char.json
@@ -414,7 +414,8 @@
             'stdio': 'ChardevStdio',
             'console': 'ChardevCommon',
             'spicevmc': { 'type': 'ChardevSpiceChannel',
-                          'if': 'defined(CONFIG_SPICE)' },
+                          'if': 'defined(CONFIG_SPICE)',
+                          'allow-flat': false },
             'spiceport': { 'type': 'ChardevSpicePort',
                            'if': 'defined(CONFIG_SPICE)' },
             'vc': 'ChardevVC',
diff --git a/docs/devel/qapi-code-gen.txt b/docs/devel/qapi-code-gen.txt
index 9722c1a204..ee34d39a20 100644
--- a/docs/devel/qapi-code-gen.txt
+++ b/docs/devel/qapi-code-gen.txt
@@ -295,7 +295,9 @@ Syntax:
               '*features': FEATURES }
     BRANCHES = { BRANCH, ... }
     BRANCH = STRING : TYPE-REF
-           | STRING : { 'type': TYPE-REF, '*if': COND }
+           | STRING : { 'type': TYPE-REF,
+                        '*if': COND,
+                        '*allow-flat': BOOL }
 
 Member 'union' names the union type.
 
@@ -334,6 +336,13 @@ values to data types like in this example:
    'data': { 'file': 'BlockdevOptionsFile',
              'qcow2': 'BlockdevOptionsQcow2' } }
 
+Simple unions can support both wrapped and flat representation of
+branches that have a struct type, unless it is explicitly disabled in
+the schema with 'allow-flat': false.  Branches of other types are
+always wrapped.  Which representation is used in the generated visitor
+C code can be configured per visitor.  Flat representation is
+appropriate when parsing command line options.
+
 In the Client JSON Protocol, all simple union branches have wrapped
 representation, as in these examples:
 
diff --git a/include/qapi/visitor-impl.h b/include/qapi/visitor-impl.h
index 7362c043be..f628b6eb36 100644
--- a/include/qapi/visitor-impl.h
+++ b/include/qapi/visitor-impl.h
@@ -121,6 +121,9 @@ struct Visitor
 
     /* Must be set */
     void (*free)(Visitor *v);
+
+    /* Set to true to make simple unions look like flat unions */
+    bool flat_simple_unions;
 };
 
 #endif
diff --git a/include/qapi/visitor.h b/include/qapi/visitor.h
index ebc19ede7f..d41be4df48 100644
--- a/include/qapi/visitor.h
+++ b/include/qapi/visitor.h
@@ -496,6 +496,16 @@ bool visit_is_input(Visitor *v);
  */
 bool visit_is_dealloc(Visitor *v);
 
+/*
+ * Check if simple unions should be treated as flat.
+ */
+bool visit_flat_simple_unions(Visitor *v);
+
+/*
+ * Set if simple unions should be treated as flat.
+ */
+void visit_set_flat_simple_unions(Visitor *v, bool flat);
+
 /*** Visiting built-in types ***/
 
 /*
diff --git a/qapi/qapi-visit-core.c b/qapi/qapi-visit-core.c
index 7e5f40e7f0..dc6fd78b8c 100644
--- a/qapi/qapi-visit-core.c
+++ b/qapi/qapi-visit-core.c
@@ -145,6 +145,16 @@ bool visit_is_dealloc(Visitor *v)
     return v->type == VISITOR_DEALLOC;
 }
 
+bool visit_flat_simple_unions(Visitor *v)
+{
+    return v->flat_simple_unions;
+}
+
+void visit_set_flat_simple_unions(Visitor *v, bool flat)
+{
+    v->flat_simple_unions = flat;
+}
+
 bool visit_type_int(Visitor *v, const char *name, int64_t *obj, Error **errp)
 {
     assert(obj);
diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 2fcaaa2497..a39092e4a9 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -248,7 +248,12 @@ def check_union(expr, info):
     for (key, value) in members.items():
         source = "'data' member '%s'" % key
         check_name_str(key, info, source)
-        check_keys(value, info, source, ['type'], ['if'])
+        check_keys(value, info, source, ['type'], ['if', 'allow-flat'])
+        if 'allow-flat' in value:
+            if discriminator is not None:
+                raise QAPISemError(info, "'allow-flat' requires simple union")
+            if not isinstance(value['allow-flat'], bool):
+                raise QAPISemError(info, "'allow-flat' must be a boolean")
         check_if(value, info, source)
         check_type(value['type'], info, source, allow_array=not base)
 
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 17525b4216..20a1acb10e 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -612,7 +612,18 @@ class QAPISchemaVariants:
             # branch do not affect another branch.  Variants that are
             # never flat don't even conflict with the base.
             if isinstance(v.type, QAPISchemaObjectType):
-                v.type.check_clash(info, dict(seen) if v.flat else {})
+                nested_seen = dict(seen) if v.flat else {}
+                v.type.check_clash(info, nested_seen)
+
+                # Make sure that the presence of a 'data' member in
+                # some input always implies wrapped representation so
+                # that visitors can unambiguously accept both forms.
+                if v.wrapped and 'data' in nested_seen:
+                    raise QAPISemError(
+                        info,
+                        "%s collides with flat representation of %s"
+                        % (nested_seen['data'].describe(info),
+                           v.describe(info)))
 
 
 class QAPISchemaMember:
@@ -721,9 +732,9 @@ class QAPISchemaVariant(QAPISchemaObjectTypeMember):
         self.wrapped = bool(wrapper_type)
         self.wrapper_type = wrapper_type
 
-        # For now, unions are either flat or wrapped, never both
+        # Unions that are both flat and wrapped can look like either one,
+        # depending on Visitor.flat_simple_unions
         assert self.flat or self.wrapped
-        assert not (self.flat and self.wrapped)
 
     def check(self, schema):
         super().check(schema)
@@ -1038,7 +1049,7 @@ class QAPISchema:
     def _make_variant(self, case, typ, ifcond, info):
         return QAPISchemaVariant(case, info, typ, ifcond)
 
-    def _make_simple_variant(self, union_name, case, typ, ifcond, info):
+    def _make_simple_variant(self, union_name, case, typ, ifcond, flat, info):
         if isinstance(typ, list):
             assert len(typ) == 1
             typ = self._make_array_type(typ[0], info)
@@ -1049,7 +1060,14 @@ class QAPISchema:
         wrapper_member = self._make_member('data', typ, None, None, info)
         wrapper_type = QAPISchemaObjectType(wrapper_name, info, None, ifcond,
                                             None, None, [wrapper_member], None)
-        return QAPISchemaVariant(case, info, typ, ifcond, flat=False,
+
+        # Default to allowing flat representation for object types.
+        # Other types require a wrapper, so disable flat for them by default.
+        if flat is None:
+            variant_type = self.resolve_type(typ, info, 'variant type')
+            flat = isinstance(variant_type, QAPISchemaObjectType)
+
+        return QAPISchemaVariant(case, info, typ, ifcond, flat=flat,
                                  wrapper_type=wrapper_type)
 
     def _def_union_type(self, expr, info, doc):
@@ -1070,9 +1088,13 @@ class QAPISchema:
                         for (key, value) in data.items()]
             members = []
         else:
-            variants = [self._make_simple_variant(name, key, value['type'],
-                                                  value.get('if'), info)
-                        for (key, value) in data.items()]
+            variants = [
+                self._make_simple_variant(name, key, value['type'],
+                                          value.get('if'),
+                                          value.get('allow-flat'),
+                                          info)
+                for (key, value) in data.items()
+            ]
             enum = [{'name': v.name, 'if': v.ifcond} for v in variants]
             typ = self._make_implicit_enum_type(name, info, ifcond, enum)
             tag_member = QAPISchemaObjectTypeMember('type', info, typ, False)
diff --git a/scripts/qapi/visit.py b/scripts/qapi/visit.py
index f72567cbcc..9d05d6bd08 100644
--- a/scripts/qapi/visit.py
+++ b/scripts/qapi/visit.py
@@ -127,23 +127,31 @@ bool visit_type_%(c_name)s_members(Visitor *v, %(c_name)s *obj, Error **errp)
                              case=case_str,
                              c_type=var.type.c_name(), c_name=c_name(var.name))
             elif var.wrapped:
+                if var.flat:
+                    cond = "!visit_flat_simple_unions(v)"
+                else:
+                    cond = "true"
                 ret += mcgen('''
     case %(case)s:
     {
         bool ok;
 
-        if (!visit_start_struct(v, "data", NULL, 0, errp)) {
-            return false;
+        if (%(cond)s) {
+            if (!visit_start_struct(v, "data", NULL, 0, errp)) {
+                return false;
+            }
         }
         ok = visit_type_%(c_type)s_members(v, &obj->u.%(c_name)s, errp);
-        if (ok) {
-            ok = visit_check_struct(v, errp);
+        if (%(cond)s) {
+            if (ok) {
+                ok = visit_check_struct(v, errp);
+            }
+            visit_end_struct(v, NULL);
         }
-        visit_end_struct(v, NULL);
         return ok;
     }
 ''',
-                             case=case_str,
+                             case=case_str, cond=cond,
                              c_type=var.type.c_name(), c_name=c_name(var.name))
 
             else:
