How to convert a structure to a public structure with custom field types with MarshalJSON

I have a Type “Book” that i read from a different interface which returns json. After reading the json and processing data, i have to convert the book to a public book type to hide fields and change output format.

My problem is, that the input type from the same field (ISBN) is sometimes string and sometimes int. I thought that the easiest solution is to use json.Number to unmarshal the data. That works - but i need string on the outgoing json on different fields…

That is the point where i need help. I would have a custom type which i can set in the public structure at the fields, where i want to set the output-json-field to string. I named the custom type “mytype” in the example. (The real data are nested and i have more fields that i set to string in the output - the id field in the public structure is only a test)

I mean, it should look something like that - or not?

func (m *mytype) MarshalJSON() ([]byte, error) {
    ...
}

Here is my example code: https://play.golang.org/p/rS9HddzDMp
package main

import (  
    "encoding/json"
    "fmt"
    "bytes"
)

/* ----------------------------------------------------------------------------------------------------
Definition of the internal Book object (read from input)
-----------------------------------------------------------------------------------------------------*/
type Book struct {
    Id                      json.Number         `json:"id"`
    Revision                int                 `json:"revision"`
    ISBN                    json.Number         `json:"isbn"`
    Title                   string              `json:"title"`
}

/* ----------------------------------------------------------------------------------------------------
Definition of the public Book object
-----------------------------------------------------------------------------------------------------*/
type AliasBook Book
type omit           *struct{}
type mytype         string

type PublicBook struct {
    Id          string          `json:"id"`
    Revision    omit            `json:"revision,omitempty"`
    ISBN        mytype          `json:"isbn"`
    *AliasBook
}

/* ----------------------------------------------------------------------------------------------------
Rendering functions
-----------------------------------------------------------------------------------------------------*/
func (bb *Book) MarshalJSON() ([]byte, error) {
    fmt.Println("---------------MarschalJSON---------------")
    aux := PublicBook{
        Id:         bb.Id.String(),
        AliasBook:  (*AliasBook)(bb),
    }

    return json.Marshal(&aux)
}

func main() {
    var jsonStreams[2][]byte
    // Input ISBN as string
    jsonStreams[0] = []byte(`{"id":"123","revision":1234,"isbn":"978-3-86680-192-9","title":"Go for dummies"}`)
    // Input ISBN as int
    jsonStreams[1] = []byte(`{"id":123,"revision":1234,"isbn":9783866801929,"title":"Go for dummies"}`)

    // For each stream
    for i := range jsonStreams {
        fmt.Print("stream: ")
        fmt.Println(string(jsonStreams[i]))

        // Read Input
        b := Book{}
        err := json.Unmarshal(jsonStreams[i], &b)
        if err == nil {
            fmt.Printf("%+v\n", b)
        } else {
            fmt.Println(err)
            fmt.Printf("%+v\n", b)
        }

        // Output as JSON
        response := new(bytes.Buffer)
        enc := json.NewEncoder(response)
        enc.SetEscapeHTML(false)
        enc.SetIndent("", "    ")
        err = enc.Encode(&b)
        if err == nil {
            fmt.Printf("%+v\n", response)
        } else {
            fmt.Println(err)
            fmt.Printf("%+v\n", response)
        }
    }
}

That’s a problem of your source then. An ISBN can’t fit into a number (speaking in json terms). The main target is JavaScript which might read that as a float…

Aside of the possibility that a number can’t correctly read in as an ISBN, you can convert any json.Number into a String by calling its String() method.

@NobbZ Thank you for your answer!
ISBN is only a example. Unmarshaling a string or integer with json.Number is working. But i don’t know how to write the marshaler to return everytime the value as string (from the field, that i marked as json.Number - in this case:ISBN)

The function i began to write was the following:

type mytype string
func (e mytype) MarshalJSON() ([]byte, error) {
    fmt.Printf("bla: %v\n", e)
    return json.Marshal(e)
}

This function is called and i know that this is not the right syntax because i get a loop and empty value. How should i write this function which return the value as string? Or is that not possible?

Or should the type be a pointer to the json.Number?
But this will not work:

type mytype json.Number
func (e mytype) MarshalJSON() ([]byte, error) {
    return json.Marshal(e.String())
}

I really do not get your problem…

If you want it to be a string, just make it string…

type Foo struct {
  Bar string `json:"bar"`
}

func FooBar(num json.Number) {
  return Foo { Bar: num.String() }
}

That Foo will be marshalled into {bar: "123123"}

1 Like

Maybe it is a problem only in my mind :slight_smile:
Input struct is with json.Number and output of the same field should be string - but only on marshaling. I would process the field intern as an integer and not as string.

type Private struct {
  Bar json.Number `json:"bar"`
}

type Public struct  {
  Bar string `json:"bar"`
}

If i declare the public struct as string in the example, the Bar field would be empty on marshaling to json. That’s why i tought i need a custom type…

Are you looking for an int attribute with the ,string tag, maybe?

Yes but that do not work for json.Number! And i use json.Number because the input on the same field can be a string or a integer

But after you have put it in your “public” struct, it will be int I thought?

You are transforming one struct into the other, the other shall be an int when accessed and string when marshalled you said, and exactly that is shown in @calmh’s example on the playground.

The type is json.Number and not int because of the different input.Thats why i am so confused about this thing…

I think this does what you want:

https://play.golang.org/p/GYSDkdHmx9

I defined ISBN as a type and then defined an UnmarshalJSON function on it that unmarshals into a json.Number and then converts that string to an ISBN.

1 Like

Yes nearly!!
The fields i have to convert should normaly be integer. (The most fields are ID’s) But the transmitter deliver sometimes string and sometimes integer. And my receiver would have these ID’s as string. So both are a little bit strange! And i want to deal with the right type which is integer. That means, That i can change my in/output to that what they want and i can deal in my program with the “right” type of my values.

So if i change your function to store the incoming value as int and “,string” in the json definition, i get a"0" value after marshal the data. Do you know why? Is there a solution to use “,string” or is that only possible if i have isbn as a int defined?

https://play.golang.org/p/pqLY7LuLOD

The docs say:

The “string” option signals that a field is stored as JSON inside a JSON-encoded string. It applies only to fields of string, floating point, integer, or boolean types.

ISBN isn’t an int (or a string, or whatever) it is an ISBN. That means ,string won’t work (which is what the returned error message states).

Here is a version of my example that represents an ISBN as an int64 (int32 overflows) that marshals to a JSON string: Go Playground - The Go Programming Language

If you want to format the string, modify ISBN.MarshalJSON.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.