omissis / go-jsonschema

A tool to generate Go data types from JSON Schema definitions.
MIT License
566 stars 90 forks source link

Generated Unmarshal for fields with `time.Time` #159

Open rbellido-ut opened 11 months ago

rbellido-ut commented 11 months ago

This JSON schema generates the following types and UnmarshalJSON method:

// CloudEvents Specification JSON Schema
type CloudeventsSchemaJson struct {
    // The event payload.
    Data Datadef `json:"data,omitempty" yaml:"data,omitempty" mapstructure:"data,omitempty"`

    // Base64 encoded event payload. Must adhere to RFC4648.
    DataBase64 DataBase64Def `json:"data_base64,omitempty" yaml:"data_base64,omitempty" mapstructure:"data_base64,omitempty"`

    // Content type of the data value. Must adhere to RFC 2046 format.
    Datacontenttype Datacontenttypedef `json:"datacontenttype,omitempty" yaml:"datacontenttype,omitempty" mapstructure:"datacontenttype,omitempty"`

    // Identifies the schema that data adheres to.
    Dataschema Dataschemadef `json:"dataschema,omitempty" yaml:"dataschema,omitempty" mapstructure:"dataschema,omitempty"`

    // Identifies the event.
    Id Iddef `json:"id" yaml:"id" mapstructure:"id"`

    // Identifies the context in which an event happened.
    Source Sourcedef `json:"source" yaml:"source" mapstructure:"source"`

    // The version of the CloudEvents specification which the event uses.
    Specversion Specversiondef `json:"specversion" yaml:"specversion" mapstructure:"specversion"`

    // Describes the subject of the event in the context of the event producer
    // (identified by source).
    Subject Subjectdef `json:"subject,omitempty" yaml:"subject,omitempty" mapstructure:"subject,omitempty"`

    // Timestamp of when the occurrence happened. Must adhere to RFC 3339.
    Time Timedef `json:"time,omitempty" yaml:"time,omitempty" mapstructure:"time,omitempty"`

    // Describes the type of event related to the originating occurrence.
    Type Typedef `json:"type" yaml:"type" mapstructure:"type"`
}

type DataBase64Def *string

type Datacontenttypedef *string

type Datadef interface{}

type Dataschemadef *string

type Iddef string

type Sourcedef string

type Specversiondef string

type Subjectdef *string

type Timedef *time.Time

type Typedef string

// UnmarshalJSON implements json.Unmarshaler.
func (j *CloudeventsSchemaJson) UnmarshalJSON(b []byte) error {
    var raw map[string]interface{}
    if err := json.Unmarshal(b, &raw); err != nil {
        return err
    }
    if v, ok := raw["id"]; !ok || v == nil {
        return fmt.Errorf("field id in CloudeventsSchemaJson: required")
    }
    if v, ok := raw["source"]; !ok || v == nil {
        return fmt.Errorf("field source in CloudeventsSchemaJson: required")
    }
    if v, ok := raw["specversion"]; !ok || v == nil {
        return fmt.Errorf("field specversion in CloudeventsSchemaJson: required")
    }
    if v, ok := raw["type"]; !ok || v == nil {
        return fmt.Errorf("field type in CloudeventsSchemaJson: required")
    }
    type Plain CloudeventsSchemaJson
    var plain Plain
    if err := json.Unmarshal(b, &plain); err != nil {
        return err
    }
    *j = CloudeventsSchemaJson(plain)
    return nil
}

The json.Unmarshal(data, &cloudevent) for it results in an error:

    err := json.Unmarshal([]byte(`{
        "data":{"exampleField":"a potato flew around", "enumField": "one potato"},
        "source":"mysource",
        "type":"public.domain.resource.action",
        "id":"7a929ab4-cd97-4208-8e49-98d9f4d88881",
        "time":"2023-08-02T17:53:08.614Z",
        "specversion":"1.0"
    }`), &cloudevent)
Failed to unmarshal JSON: &json.UnmarshalTypeError{Value:"string", Type:(*reflect.rtype)(0x11b6de0), Offset:88, Struct:"Plain", Field:"time"}

Is there a way for the generated UnmarshalJSON to account for Unmarshalling the Time field?

We've resorted to implementing our own UnmarshalJSON and passing --only-models in the generate command for the time being.

omissis commented 10 months ago

Hi @rbellido-ut thanks for reporting this issue, I will look into it!

omissis commented 10 months ago

@rbellido-ut I can't seem to reproduce your error, but I get a different one as time does not get imported correctly. what version of go-jsonschema are you using? Also, can you share the whole generated go file, including imports, please?

omissis commented 10 months ago

I investigated the matter a bit today and it seems the problem is a bit more complex than I expected, as we are dealing with a nullable reference type here, which makes implementing custom unmarshalling the value correctly a bit of a challenge. I will try to see if I can find a solution in the next days, let's see.

andrewpollock commented 10 months ago

Hi,

I think I may have a related problem.

I'm operating on https://csrc.nist.gov/schema/nvd/api/2.0/cve_api_json_2.0.schema, and having invoked it as go-jsonschema -p cves /tmp/cve_api_json_2.0.schema have ended up with this particular struct:

type CveApiJson20Schema struct {
        // Format corresponds to the JSON schema field "format".
        Format string `json:"format" yaml:"format" mapstructure:"format"`

        // ResultsPerPage corresponds to the JSON schema field "resultsPerPage".
        ResultsPerPage int `json:"resultsPerPage" yaml:"resultsPerPage" mapstructure:"resultsPerPage"`

        // StartIndex corresponds to the JSON schema field "startIndex".
        StartIndex int `json:"startIndex" yaml:"startIndex" mapstructure:"startIndex"`

        // Timestamp corresponds to the JSON schema field "timestamp".
        Timestamp time.Time `json:"timestamp" yaml:"timestamp" mapstructure:"timestamp"`

        // TotalResults corresponds to the JSON schema field "totalResults".
        TotalResults int `json:"totalResults" yaml:"totalResults" mapstructure:"totalResults"`

        // Version corresponds to the JSON schema field "version".
        Version string `json:"version" yaml:"version" mapstructure:"version"`

        // NVD feed array of CVE
        Vulnerabilities []CveItem `json:"vulnerabilities" yaml:"vulnerabilities" mapstructure:"vulnerabilities"`
}

A sample of what I'm trying to decode is (see https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2023-4863):

{
"resultsPerPage": 1,
"startIndex": 0,
"totalResults": 1,
"format": "NVD_CVE",
"version": "2.0",
"timestamp": "2023-11-21T09:56:39.367",
"vulnerabilities": []
}

Whilst debugging why decoding was failing, I noticed this error:

time.ParseError {Layout: "2006-01-02T15:04:05Z07:00", Value: "2023-11-20T01:00:26.840", LayoutElem: "Z07:00", ValueElem: "", Message: ""}

I've been having some fun decoding the ISO 8601 format the NVD is using (see also https://mastodon.au/@andrewpollock/111447117663929594)

I'm not actually understanding how the time format "2006-01-02T15:04:05Z07:00" is getting arrived at. I know this is the time.RFC3339 constant, but I'm not seeing how things are defaulting to this? I think if I can override this (somewhere) to be "2006-01-02T15:04:05.999" instead, I might be in business...

dstubbersfield commented 4 months ago

@omissis @rbellido-ut wondering if you found a workaround to this? I believe I'm observing the same bug, before marshaling, my time.Time object is DateTime:{wall:134877000 ext:63851803830 loc:0x196c7e0} and afterwards DateTime:{wall:0 ext:0 loc:<nil>}

omissis commented 2 days ago

hey @dstubbersfield unfortunately I haven't. I am trying to prioritise the open PRs over the issues, hopefully I will get there soon enough.