How to use generics in Unmarshal (go 1.18)

Issue

I’m new to golang generics and have the following setup.

  1. I’ve gather loads of different kinds of reports.
  2. Each report do have enclosing fields
  3. So I wrap it in a ReportContainerImpl

I’ve use a type argument of [T Reportable] where the Reportable is defined as follows

type Reportable interface {
    ExportDataPointReport | ImportDataPointReport | MissingDataPointReport | SensorThresoldReport
}

Each of the type in the type constraint is structs that is to be embedded in the container.

type ReportContainerImpl[T Reportable] struct {
    LocationID string `json:"lid"`
    Provider string `json:"pn"`
    ReportType ReportType `json:"m"`
    Body T `json:"body"`
}

I use a discriminator ReportType to determine the concrete type when Unmarshal.

type ReportType string

const (
    ReportTypeExportDataPointReport ReportType = "ExportDataPointReport"
    ReportTypeImportDataPointReport ReportType = "ImportDataPointReport"
    ReportTypeMissingDataPointReport ReportType = "MissingDataPointReport"
    ReportTypeSensorThresoldReport ReportType = "SensorThresoldReport"
)

Since go do not support type assertion for struct (only interfaces) it is not possible to cast the type when Unmarshal. Also go do not support pointer to the "raw" generic type. Hence, I’ve created a interface that the ReportContainerImpl implements.

type ReportContainer interface {
    GetLocationID() string
    GetProvider() string
    GetReportType() ReportType
    GetBody() interface{}
}

The problem I then get is that I cannot do type constrains on the return type in any form or shape and are back at "freetext semantics" on the GetBody() function to allow for type assertion when Unmarshal is done.

    container, err := UnmarshalReportContainer(data)

    if rep, ok := container.GetBody().(ExportDataPointReport); ok {
      // Use the ReportContainerImpl[ExportDataPointReport] here...
    }

Maybe I’m getting this wrong? – but however I do this, I always end up with somewhere needs a interface{} or to know the exact type before Unmarshal

  • Do you have a better suggestion how to solve this in a type (safer) way?

Cheers,
Mario 🙂

For completeness I add the UnmarshalReportContainer here

func UnmarshalReportContainer(data []byte) (ReportContainer, error) {

    type Temp struct {
        LocationID string `json:"lid"`
        Provider string `json:"pn"`
        ReportType ReportType `json:"m"`
        Body *json.RawMessage `json:"body"`
    }

    var temp Temp
    err := json.Unmarshal(data, &temp)
    if err != nil {
        return nil, err
    }

    switch temp.ReportType {
    case ReportTypeExportDataPointReport:
        var report ExportDataPointReport
        err := json.Unmarshal(*temp.Body, &report)
        return &ReportContainerImpl[ExportDataPointReport]{
            LocationID: temp.LocationID,
            Provider:   temp.Provider,
            ReportType: temp.ReportType,
            Body:       report,
        }, err

      // ...
    }
}

Solution

but however I do this, I always end up with somewhere needs a interface{} or to know the exact type before Unmarshal

Precisely.

The concrete types needed to instantiate some generic type or function like ReportContainerImpl or UnmarshalReportContainer must be known at compile time, when you write the code. JSON unmarshalling instead occurs at run-time, when you have the byte slice populated with the actual data.

To unmarshal dynamic JSON based on some discriminatory value, you still need a switch.

Do you have a better suggestion how to solve this in a type (safer) way?

Just forgo parametric polymorphism. It’s not a good fit here. Keep the code you have now with json.RawMessage, unmarshal the dynamic data conditionally in the switch and return the concrete structs that implement ReportContainer interface.


As a side note, if you find yourself writing unions with several struct types, it’s also a sign you might need to use plain old interfaces instead of type parameters.

Answered By – blackgreen

Answer Checked By – Pedro (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.