diff mbox series

[v2,04/11] qapi: golang: Generate qapi's alternate types in Go

Message ID 20231016152704.221611-5-victortoso@redhat.com (mailing list archive)
State New, archived
Headers show
Series qapi-go: add generator for Golang interface | expand

Commit Message

Victor Toso Oct. 16, 2023, 3:26 p.m. UTC
This patch handles QAPI alternate types and generates data structures
in Go that handles it.

Alternate types are similar to Union but without a discriminator that
can be used to identify the underlying value on the wire. It is needed
to infer it. In Go, most of the types [*] are mapped as optional
fields and Marshal and Unmarshal methods will be handling the data
checks.

Example:

qapi:
  | { 'alternate': 'BlockdevRef',
  |   'data': { 'definition': 'BlockdevOptions',
  |             'reference': 'str' } }

go:
  | type BlockdevRef struct {
  |         Definition *BlockdevOptions
  |         Reference  *string
  | }

usage:
  | input := `{"driver":"qcow2","data-file":"/some/place/my-image"}`
  | k := BlockdevRef{}
  | err := json.Unmarshal([]byte(input), &k)
  | if err != nil {
  |     panic(err)
  | }
  | // *k.Definition.Qcow2.DataFile.Reference == "/some/place/my-image"

[*] The exception for optional fields as default is to Types that can
accept JSON Null as a value. For this case, we translate NULL to a
member type called IsNull, which is boolean in Go.  This will be
explained better in the documentation patch of this series but the
main rationale is around Marshaling to and from JSON and Go data
structures.

Example:

qapi:
 | { 'alternate': 'StrOrNull',
 |   'data': { 's': 'str',
 |             'n': 'null' } }

go:
 | type StrOrNull struct {
 |     S      *string
 |     IsNull bool
 | }

Signed-off-by: Victor Toso <victortoso@redhat.com>
---
 scripts/qapi/golang.py | 301 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 298 insertions(+), 3 deletions(-)

Comments

Andrea Bolognani Nov. 6, 2023, 3:28 p.m. UTC | #1
On Mon, Oct 16, 2023 at 05:26:57PM +0200, Victor Toso wrote:
> This patch handles QAPI alternate types and generates data structures
> in Go that handles it.
>
> Alternate types are similar to Union but without a discriminator that
> can be used to identify the underlying value on the wire. It is needed
> to infer it. In Go, most of the types [*] are mapped as optional
> fields and Marshal and Unmarshal methods will be handling the data
> checks.
>
> Example:
>
> qapi:
>   | { 'alternate': 'BlockdevRef',
>   |   'data': { 'definition': 'BlockdevOptions',
>   |             'reference': 'str' } }
>
> go:
>   | type BlockdevRef struct {
>   |         Definition *BlockdevOptions
>   |         Reference  *string
>   | }
>
> usage:
>   | input := `{"driver":"qcow2","data-file":"/some/place/my-image"}`
>   | k := BlockdevRef{}
>   | err := json.Unmarshal([]byte(input), &k)
>   | if err != nil {
>   |     panic(err)
>   | }
>   | // *k.Definition.Qcow2.DataFile.Reference == "/some/place/my-image"
>
> [*] The exception for optional fields as default is to Types that can
> accept JSON Null as a value. For this case, we translate NULL to a
> member type called IsNull, which is boolean in Go.  This will be
> explained better in the documentation patch of this series but the
> main rationale is around Marshaling to and from JSON and Go data
> structures.

These usage examples are great; in fact, I think they're too good to
be relegated to the commit messages. I would like to see them in the
actual documentation.

At the same time, the current documentation seems to focus a lot on
internals rather than usage. I think we really need two documents
here:

  * one for users of the library, with lots of usage examples
    (ideally at least one for JSON->Go and one for Go->JSON for each
    class of QAPI object) and little-to-no peeking behind the
    curtains;

  * one for QEMU developers / QAPI maintainers, which goes into
    detail regarding the internals and explains the various design
    choices and trade-offs.

Some parts of the latter should probably be code comments instead. A
reasonable balance will have to be found.

> diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
> +TEMPLATE_HELPER = """
> +// Creates a decoder that errors on unknown Fields
> +// Returns nil if successfully decoded @from payload to @into type
> +// Returns error if failed to decode @from payload to @into type
> +func StrictDecode(into interface{}, from []byte) error {
> +\tdec := json.NewDecoder(strings.NewReader(string(from)))
> +\tdec.DisallowUnknownFields()
> +
> +\tif err := dec.Decode(into); err != nil {
> +\t\treturn err
> +\t}
> +\treturn nil
> +}
> +"""

I think the use of \t here makes things a lot less readable. Can't
you just do

  TEMPLATE_HELPER = """
  func StrictDecode() {
      dec := ...
      if err := ... {
         return err
      }
      return nil
  }
  """

I would actually recommend the use of textwrap.dedent() to make
things less awkward:

  TEMPLATE_HELPER = textwrap.dedent("""
      func StrictDecode() {
          dec := ...
          if err := ... {
             return err
          }
          return nil
      }
  """

This is particularly useful for blocks of Go code that are not
declared at the top level...

> +        unmarshal_check_fields = f"""
> +\t// Check for json-null first
> +\tif string(data) == "null" {{
> +\t\treturn errors.New(`null not supported for {name}`)
> +\t}}"""

... such as this one, which could be turned into:

  unmarshal_check_fields = textwrap.dedent(f"""
      // Check for json-null first
      if string(data) == "null" {{
          return errors.New(`null not supported for {name}`)
      }}
  """)

Much more manageable, don't you think? :)


On a partially related note: while I haven't yet looked closely at
how much effort you've dedicated to producing pretty output, from a
quick look at generate_struct_type() it seems that the answer is "not
zero". I think it would be fine to simplify things there and produce
ugly output, under the assumption that gofmt will be called on the
generated code immediately afterwards. The C generator doesn't have
this luxury, but we should take advantage of it.
Victor Toso Nov. 6, 2023, 3:52 p.m. UTC | #2
Hi,

On Mon, Nov 06, 2023 at 07:28:04AM -0800, Andrea Bolognani wrote:
> On Mon, Oct 16, 2023 at 05:26:57PM +0200, Victor Toso wrote:
> > This patch handles QAPI alternate types and generates data structures
> > in Go that handles it.
> >
> > Alternate types are similar to Union but without a discriminator that
> > can be used to identify the underlying value on the wire. It is needed
> > to infer it. In Go, most of the types [*] are mapped as optional
> > fields and Marshal and Unmarshal methods will be handling the data
> > checks.
> >
> > Example:
> >
> > qapi:
> >   | { 'alternate': 'BlockdevRef',
> >   |   'data': { 'definition': 'BlockdevOptions',
> >   |             'reference': 'str' } }
> >
> > go:
> >   | type BlockdevRef struct {
> >   |         Definition *BlockdevOptions
> >   |         Reference  *string
> >   | }
> >
> > usage:
> >   | input := `{"driver":"qcow2","data-file":"/some/place/my-image"}`
> >   | k := BlockdevRef{}
> >   | err := json.Unmarshal([]byte(input), &k)
> >   | if err != nil {
> >   |     panic(err)
> >   | }
> >   | // *k.Definition.Qcow2.DataFile.Reference == "/some/place/my-image"
> >
> > [*] The exception for optional fields as default is to Types that can
> > accept JSON Null as a value. For this case, we translate NULL to a
> > member type called IsNull, which is boolean in Go.  This will be
> > explained better in the documentation patch of this series but the
> > main rationale is around Marshaling to and from JSON and Go data
> > structures.
> 
> These usage examples are great; in fact, I think they're too good to
> be relegated to the commit messages. I would like to see them in the
> actual documentation.
> 
> At the same time, the current documentation seems to focus a lot on
> internals rather than usage. I think we really need two documents
> here:
> 
>   * one for users of the library, with lots of usage examples
>     (ideally at least one for JSON->Go and one for Go->JSON for each
>     class of QAPI object) and little-to-no peeking behind the
>     curtains;
> 
>   * one for QEMU developers / QAPI maintainers, which goes into
>     detail regarding the internals and explains the various design
>     choices and trade-offs.
> 
> Some parts of the latter should probably be code comments instead. A
> reasonable balance will have to be found.
> 
> > diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
> > +TEMPLATE_HELPER = """
> > +// Creates a decoder that errors on unknown Fields
> > +// Returns nil if successfully decoded @from payload to @into type
> > +// Returns error if failed to decode @from payload to @into type
> > +func StrictDecode(into interface{}, from []byte) error {
> > +\tdec := json.NewDecoder(strings.NewReader(string(from)))
> > +\tdec.DisallowUnknownFields()
> > +
> > +\tif err := dec.Decode(into); err != nil {
> > +\t\treturn err
> > +\t}
> > +\treturn nil
> > +}
> > +"""
> 
> I think the use of \t here makes things a lot less readable. Can't
> you just do
> 
>   TEMPLATE_HELPER = """
>   func StrictDecode() {
>       dec := ...
>       if err := ... {
>          return err
>       }
>       return nil
>   }
>   """
> 
> I would actually recommend the use of textwrap.dedent() to make
> things less awkward:
> 
>   TEMPLATE_HELPER = textwrap.dedent("""
>       func StrictDecode() {
>           dec := ...
>           if err := ... {
>              return err
>           }
>           return nil
>       }
>   """
> 
> This is particularly useful for blocks of Go code that are not
> declared at the top level...
> 
> > +        unmarshal_check_fields = f"""
> > +\t// Check for json-null first
> > +\tif string(data) == "null" {{
> > +\t\treturn errors.New(`null not supported for {name}`)
> > +\t}}"""
> 
> ... such as this one, which could be turned into:
> 
>   unmarshal_check_fields = textwrap.dedent(f"""
>       // Check for json-null first
>       if string(data) == "null" {{
>           return errors.New(`null not supported for {name}`)
>       }}
>   """)
> 
> Much more manageable, don't you think? :)

Didn't know this one, I agree 100% that is nicer. I'll take a
look for the next iteration if the output would still be similar
as I want to gofmt change be zero (see bellow).
 
> 
> On a partially related note: while I haven't yet looked closely at
> how much effort you've dedicated to producing pretty output, from a
> quick look at generate_struct_type() it seems that the answer is "not
> zero". I think it would be fine to simplify things there and produce
> ugly output, under the assumption that gofmt will be called on the
> generated code immediately afterwards. The C generator doesn't have
> this luxury, but we should take advantage of it.

Yes, I wholeheartedly agree. The idea of the generator producing
a well formatted Go code came from previous review. I didn't want
to introduce gofmt and friends to QEMU build system, perhaps it
wasn't a big deal but I find it foreign to QEMU for a generated
code that QEMU itself would not use.

See: https://lists.gnu.org/archive/html/qemu-devel/2023-10/msg01188.html

Cheers,
Victor
Andrea Bolognani Nov. 6, 2023, 4:12 p.m. UTC | #3
On Mon, Nov 06, 2023 at 04:52:12PM +0100, Victor Toso wrote:
> On Mon, Nov 06, 2023 at 07:28:04AM -0800, Andrea Bolognani wrote:
> > On a partially related note: while I haven't yet looked closely at
> > how much effort you've dedicated to producing pretty output, from a
> > quick look at generate_struct_type() it seems that the answer is "not
> > zero". I think it would be fine to simplify things there and produce
> > ugly output, under the assumption that gofmt will be called on the
> > generated code immediately afterwards. The C generator doesn't have
> > this luxury, but we should take advantage of it.
>
> Yes, I wholeheartedly agree. The idea of the generator producing
> a well formatted Go code came from previous review. I didn't want
> to introduce gofmt and friends to QEMU build system, perhaps it
> wasn't a big deal but I find it foreign to QEMU for a generated
> code that QEMU itself would not use.
>
> See: https://lists.gnu.org/archive/html/qemu-devel/2023-10/msg01188.html

Noted.

Whether or not requiring a pass through gofmt is an issue kind of
depends on how we end up shipping these files.

What seems the most probable to me is that we'll have a separate repo
where we dump the generated files and that Go users will consume via
a regular 'import'. Your existing qapi-go repo follows this model. In
this context, gofmt is never going to be called as part of the QEMU
build process so it doesn't really matter.

But maybe there was a discussion around this that I've missed :)
Andrea Bolognani Nov. 9, 2023, 5:34 p.m. UTC | #4
On Mon, Oct 16, 2023 at 05:26:57PM +0200, Victor Toso wrote:
> [*] The exception for optional fields as default is to Types that can
> accept JSON Null as a value. For this case, we translate NULL to a
> member type called IsNull, which is boolean in Go.  This will be
> explained better in the documentation patch of this series but the
> main rationale is around Marshaling to and from JSON and Go data
> structures.
>
> Example:
>
> qapi:
>  | { 'alternate': 'StrOrNull',
>  |   'data': { 's': 'str',
>  |             'n': 'null' } }
>
> go:
>  | type StrOrNull struct {
>  |     S      *string
>  |     IsNull bool
>  | }

We should call this Null instead of IsNull, and also make it a
pointer similarly to what I just suggested for union branches that
don't have additional data attached to them.
Victor Toso Nov. 9, 2023, 7:01 p.m. UTC | #5
Hi,

On Thu, Nov 09, 2023 at 09:34:56AM -0800, Andrea Bolognani wrote:
> On Mon, Oct 16, 2023 at 05:26:57PM +0200, Victor Toso wrote:
> > [*] The exception for optional fields as default is to Types that can
> > accept JSON Null as a value. For this case, we translate NULL to a
> > member type called IsNull, which is boolean in Go.  This will be
> > explained better in the documentation patch of this series but the
> > main rationale is around Marshaling to and from JSON and Go data
> > structures.
> >
> > Example:
> >
> > qapi:
> >  | { 'alternate': 'StrOrNull',
> >  |   'data': { 's': 'str',
> >  |             'n': 'null' } }
> >
> > go:
> >  | type StrOrNull struct {
> >  |     S      *string
> >  |     IsNull bool
> >  | }
> 
> We should call this Null instead of IsNull, and also make it a
> pointer similarly to what I just suggested for union branches
> that don't have additional data attached to them.

I don't have a strong argument here against what you suggest, I
just think that the usage of bool is simpler.

The test I have here [0] would have code changing to something
like:

When is null:

  - val := &StrOrNull{IsNull: true}
  + myNull := JSONNull{}
  + val := &StrOrNull{Null: &myNull}

So you need a instance to get a pointer.

When is absent (no fields set), no changes as both bool defaults
to false and pointer to nil.

The code handling this would change from:

  - if u.IsNull {
  + if u.Null != nil {
        ...
    }

We don't have the same issues as Union, under the hood we Marshal
to/Unmarshal from "null" and that would not change.

[0] https://gitlab.com/victortoso/qapi-go/-/blob/main/test/type_or_null_test.go

I can change this in the next iteration.

Cheers,
Victor
Andrea Bolognani Nov. 10, 2023, 9:58 a.m. UTC | #6
On Thu, Nov 09, 2023 at 08:01:48PM +0100, Victor Toso wrote:
> On Thu, Nov 09, 2023 at 09:34:56AM -0800, Andrea Bolognani wrote:
> > We should call this Null instead of IsNull, and also make it a
> > pointer similarly to what I just suggested for union branches
> > that don't have additional data attached to them.
>
> I don't have a strong argument here against what you suggest, I
> just think that the usage of bool is simpler.
>
> The test I have here [0] would have code changing to something
> like:
>
> When is null:
>
>   - val := &StrOrNull{IsNull: true}
>   + myNull := JSONNull{}
>   + val := &StrOrNull{Null: &myNull}
>
> So you need a instance to get a pointer.
>
> When is absent (no fields set), no changes as both bool defaults
> to false and pointer to nil.
>
> The code handling this would change from:
>
>   - if u.IsNull {
>   + if u.Null != nil {
>         ...
>     }
>
> We don't have the same issues as Union, under the hood we Marshal
> to/Unmarshal from "null" and that would not change.
>
> [0] https://gitlab.com/victortoso/qapi-go/-/blob/main/test/type_or_null_test.go
>
> I can change this in the next iteration.

No, leave the type alone. But I still think the name should probably
be Null instead of IsNull.
Victor Toso Nov. 10, 2023, 3:43 p.m. UTC | #7
Hi,

On Fri, Nov 10, 2023 at 01:58:20AM -0800, Andrea Bolognani wrote:
> On Thu, Nov 09, 2023 at 08:01:48PM +0100, Victor Toso wrote:
> > On Thu, Nov 09, 2023 at 09:34:56AM -0800, Andrea Bolognani wrote:
> > > We should call this Null instead of IsNull, and also make it a
> > > pointer similarly to what I just suggested for union branches
> > > that don't have additional data attached to them.
> >
> > I don't have a strong argument here against what you suggest, I
> > just think that the usage of bool is simpler.
> >
> > The test I have here [0] would have code changing to something
> > like:
> >
> > When is null:
> >
> >   - val := &StrOrNull{IsNull: true}
> >   + myNull := JSONNull{}
> >   + val := &StrOrNull{Null: &myNull}
> >
> > So you need a instance to get a pointer.
> >
> > When is absent (no fields set), no changes as both bool defaults
> > to false and pointer to nil.
> >
> > The code handling this would change from:
> >
> >   - if u.IsNull {
> >   + if u.Null != nil {
> >         ...
> >     }
> >
> > We don't have the same issues as Union, under the hood we Marshal
> > to/Unmarshal from "null" and that would not change.
> >
> > [0] https://gitlab.com/victortoso/qapi-go/-/blob/main/test/type_or_null_test.go
> >
> > I can change this in the next iteration.
> 
> No, leave the type alone. But I still think the name should probably
> be Null instead of IsNull.

Ok, keeping bool, rename to Null. Deal.

Cheers,
Victor
diff mbox series

Patch

diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
index dc12be7b03..3f6692df4b 100644
--- a/scripts/qapi/golang.py
+++ b/scripts/qapi/golang.py
@@ -13,10 +13,11 @@ 
 from __future__ import annotations
 
 import os
-from typing import List, Optional
+from typing import List, Optional, Tuple
 
 from .schema import (
     QAPISchema,
+    QAPISchemaAlternateType,
     QAPISchemaEnumMember,
     QAPISchemaFeature,
     QAPISchemaIfCond,
@@ -37,6 +38,77 @@ 
 )
 """
 
+TEMPLATE_HELPER = """
+// Creates a decoder that errors on unknown Fields
+// Returns nil if successfully decoded @from payload to @into type
+// Returns error if failed to decode @from payload to @into type
+func StrictDecode(into interface{}, from []byte) error {
+\tdec := json.NewDecoder(strings.NewReader(string(from)))
+\tdec.DisallowUnknownFields()
+
+\tif err := dec.Decode(into); err != nil {
+\t\treturn err
+\t}
+\treturn nil
+}
+"""
+
+TEMPLATE_ALTERNATE = """
+// Only implemented on Alternate types that can take JSON NULL as value.
+//
+// This is a helper for the marshalling code. It should return true only when
+// the Alternate is empty (no members are set), otherwise it returns false and
+// the member set to be Marshalled.
+type AbsentAlternate interface {
+\tToAnyOrAbsent() (any, bool)
+}
+"""
+
+TEMPLATE_ALTERNATE_NULLABLE_CHECK = """
+\t\t}} else if s.{var_name} != nil {{
+\t\t\treturn *s.{var_name}, false"""
+
+TEMPLATE_ALTERNATE_MARSHAL_CHECK = """
+\tif s.{var_name} != nil {{
+\t\treturn json.Marshal(s.{var_name})
+\t}} else """
+
+TEMPLATE_ALTERNATE_UNMARSHAL_CHECK = """
+\t// Check for {var_type}
+\t{{
+\t\ts.{var_name} = new({var_type})
+\t\tif err := StrictDecode(s.{var_name}, data); err == nil {{
+\t\t\treturn nil
+\t\t}}
+\t\ts.{var_name} = nil
+\t}}
+"""
+
+TEMPLATE_ALTERNATE_NULLABLE = """
+func (s *{name}) ToAnyOrAbsent() (any, bool) {{
+\tif s != nil {{
+\t\tif s.IsNull {{
+\t\t\treturn nil, false
+{absent_check_fields}
+\t\t}}
+\t}}
+
+\treturn nil, true
+}}
+"""
+
+TEMPLATE_ALTERNATE_METHODS = """
+func (s {name}) MarshalJSON() ([]byte, error) {{
+\t{marshal_check_fields}
+\treturn {marshal_return_default}
+}}
+
+func (s *{name}) UnmarshalJSON(data []byte) error {{
+{unmarshal_check_fields}
+\treturn fmt.Errorf("Can't convert to {name}: %s", string(data))
+}}
+"""
+
 
 def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
     vis = QAPISchemaGenGolangVisitor(prefix)
@@ -44,10 +116,191 @@  def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
     vis.write(output_dir)
 
 
+def qapi_to_field_name(name: str) -> str:
+    return name.title().replace("_", "").replace("-", "")
+
+
 def qapi_to_field_name_enum(name: str) -> str:
     return name.title().replace("-", "")
 
 
+def qapi_schema_type_to_go_type(qapitype: str) -> str:
+    schema_types_to_go = {
+        "str": "string",
+        "null": "nil",
+        "bool": "bool",
+        "number": "float64",
+        "size": "uint64",
+        "int": "int64",
+        "int8": "int8",
+        "int16": "int16",
+        "int32": "int32",
+        "int64": "int64",
+        "uint8": "uint8",
+        "uint16": "uint16",
+        "uint32": "uint32",
+        "uint64": "uint64",
+        "any": "any",
+        "QType": "QType",
+    }
+
+    prefix = ""
+    if qapitype.endswith("List"):
+        prefix = "[]"
+        qapitype = qapitype[:-4]
+
+    qapitype = schema_types_to_go.get(qapitype, qapitype)
+    return prefix + qapitype
+
+
+def qapi_field_to_go_field(
+    member_name: str, type_name: str
+) -> Tuple[str, str, str]:
+    # Nothing to generate on null types. We update some
+    # variables to handle json-null on marshalling methods.
+    if type_name == "null":
+        return "IsNull", "bool", ""
+
+    # This function is called on Alternate, so fields should be ptrs
+    return (
+        qapi_to_field_name(member_name),
+        qapi_schema_type_to_go_type(type_name),
+        "*",
+    )
+
+
+# Helper function for boxed or self contained structures.
+def generate_struct_type(
+    type_name, args: List[dict[str:str]] = None, ident: int = 0
+) -> str:
+    members = "{}"
+    base_ident = "\t" * ident
+    if args is not None:
+        # Most of the logic below is to mimic the gofmt tool.
+        # We calculate spaces between member and type and between
+        # the type and tag.  Note that gofmt considers comments as
+        # divider between ident blocks.
+        maxname, maxtype = 0, 0
+        blocks: tuple(int, int) = []
+        for arg in args:
+            if "comment" in arg:
+                blocks.append((maxname, maxtype))
+                maxname, maxtype = 0, 0
+                continue
+
+            if "type" not in arg:
+                continue
+
+            maxname = max(maxname, len(arg["name"]))
+            maxtype = max(maxtype, len(arg["type"]))
+
+        blocks.append((maxname, maxtype))
+        block = 0
+
+        maxname, maxtype = blocks[0]
+        members = " {\n"
+        for arg in args:
+            if "comment" in arg:
+                block += 1
+                maxname, maxtype = blocks[block]
+                members += f"""\t// {arg["comment"]}\n"""
+                continue
+
+            name2type = ""
+            if "type" in arg:
+                name2type = " " * (maxname - len(arg["name"]) + 1)
+            type2tag = ""
+            if "tag" in arg:
+                type2tag = " " * (maxtype - len(arg["type"]) + 1)
+
+            fident = "\t" * (ident + 1)
+            gotype = "" if "type" not in arg else arg["type"]
+            tag = "" if "tag" not in arg else arg["tag"]
+            name = arg["name"]
+            members += (
+                f"""{fident}{name}{name2type}{gotype}{type2tag}{tag}\n"""
+            )
+        members += f"{base_ident}}}\n"
+
+    with_type = f"\n{base_ident}type {type_name}" if len(type_name) > 0 else ""
+    return f"""{with_type} struct{members}"""
+
+
+def generate_template_alternate(
+    self: QAPISchemaGenGolangVisitor,
+    name: str,
+    variants: Optional[QAPISchemaVariants],
+) -> str:
+    absent_check_fields = ""
+    args: List[dict[str:str]] = []
+    # to avoid having to check accept_null_types
+    nullable = False
+    if name in self.accept_null_types:
+        # In QEMU QAPI schema, only StrOrNull and BlockdevRefOrNull.
+        nullable = True
+        marshal_return_default = """[]byte("{}"), nil"""
+        marshal_check_fields = """if s.IsNull {
+\t\treturn []byte("null"), nil
+\t} else """
+        unmarshal_check_fields = """
+\t// Check for json-null first
+\tif string(data) == "null" {
+\t\ts.IsNull = true
+\t\treturn nil
+\t}"""
+    else:
+        marshal_return_default = f'nil, errors.New("{name} has empty fields")'
+        marshal_check_fields = ""
+        unmarshal_check_fields = f"""
+\t// Check for json-null first
+\tif string(data) == "null" {{
+\t\treturn errors.New(`null not supported for {name}`)
+\t}}"""
+
+    if variants:
+        for var in variants.variants:
+            var_name, var_type, isptr = qapi_field_to_go_field(
+                var.name, var.type.name
+            )
+            args.append(
+                {
+                    "name": f"{var_name}",
+                    "type": f"{isptr}{var_type}",
+                }
+            )
+
+            # Null is special, handled first
+            if var.type.name == "null":
+                assert nullable
+                continue
+
+            if nullable:
+                absent_check_fields += (
+                    TEMPLATE_ALTERNATE_NULLABLE_CHECK.format(var_name=var_name)
+                )
+            marshal_check_fields += TEMPLATE_ALTERNATE_MARSHAL_CHECK[
+                2:
+            ].format(var_name=var_name)
+            unmarshal_check_fields += (
+                TEMPLATE_ALTERNATE_UNMARSHAL_CHECK.format(
+                    var_name=var_name, var_type=var_type
+                )
+            )
+
+    content = generate_struct_type(name, args)
+    if nullable:
+        content += TEMPLATE_ALTERNATE_NULLABLE.format(
+            name=name, absent_check_fields=absent_check_fields
+        )
+    content += TEMPLATE_ALTERNATE_METHODS.format(
+        name=name,
+        marshal_check_fields=marshal_check_fields[:-6],
+        marshal_return_default=marshal_return_default,
+        unmarshal_check_fields=unmarshal_check_fields[1:],
+    )
+    return content
+
+
 def generate_content_from_dict(data: dict[str, str]) -> str:
     content = ""
 
@@ -61,22 +314,60 @@  class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
     # pylint: disable=too-many-arguments
     def __init__(self, _: str):
         super().__init__()
-        types = ("enum",)
+        types = (
+            "alternate",
+            "enum",
+            "helper",
+        )
         self.target = dict.fromkeys(types, "")
         self.schema: QAPISchema
         self.golang_package_name = "qapi"
         self.enums: dict[str, str] = {}
+        self.alternates: dict[str, str] = {}
+        self.accept_null_types = []
 
     def visit_begin(self, schema: QAPISchema) -> None:
         self.schema = schema
 
+        # We need to be aware of any types that accept JSON NULL
+        for name, entity in self.schema._entity_dict.items():
+            if not isinstance(entity, QAPISchemaAlternateType):
+                # Assume that only Alternate types accept JSON NULL
+                continue
+
+            for var in entity.variants.variants:
+                if var.type.name == "null":
+                    self.accept_null_types.append(name)
+                    break
+
         # Every Go file needs to reference its package name
+        # and most have some imports too.
         for target in self.target:
             self.target[target] = f"package {self.golang_package_name}\n"
 
+            if target == "helper":
+                imports = """\nimport (
+\t"encoding/json"
+\t"strings"
+)
+"""
+            else:
+                imports = """\nimport (
+\t"encoding/json"
+\t"errors"
+\t"fmt"
+)
+"""
+            if target != "enum":
+                self.target[target] += imports
+
+        self.target["helper"] += TEMPLATE_HELPER
+        self.target["alternate"] += TEMPLATE_ALTERNATE
+
     def visit_end(self) -> None:
         del self.schema
         self.target["enum"] += generate_content_from_dict(self.enums)
+        self.target["alternate"] += generate_content_from_dict(self.alternates)
 
     def visit_object_type(
         self,
@@ -98,7 +389,11 @@  def visit_alternate_type(
         features: List[QAPISchemaFeature],
         variants: QAPISchemaVariants,
     ) -> None:
-        pass
+        assert name not in self.alternates
+
+        self.alternates[name] = generate_template_alternate(
+            self, name, variants
+        )
 
     def visit_enum_type(
         self,