如何正确地将带有String()方法的嵌入式结构序列化为JSON字符串?

 package main

 import (
    "fmt"
    "encoding/json"
 )

 type Ticket struct {
    From string
    To   string
}

func (t Ticket) String() string {
    return fmt.Sprintf("%s - %s", t.From, t.To)
}

type Passenger struct {
    Name string `json:"Name"`
    Tkt  Ticket `json:"Ticket"`
}

func main() {
    p := Passenger{}
    p.Name = "John"
    p.Tkt.From = "New York"
    p.Tkt.To = "Washington"

    buf, _ := json.Marshal(p)
    fmt.Println(string(buf))
}

This code outputs:

 {"Name":"John","Ticket":{"From":"New York","To":"Washington"}}

But, using json.Marshal() method (it's easy and friendly for complex struct), how to make it output like this:

 {"Name":"John","Ticket":"New York - Washington"}

To generate the JSON representation of a Go value, the encoding/json package checks if the value implements the json.Marshaler or the encoding.TextMarshaler interfaces, and if so, they are used / called (in this order). This is documented at json.Marshal():

Marshal traverses the value v recursively. If an encountered value implements the Marshaler interface and is not a nil pointer, Marshal calls its MarshalJSON method to produce JSON. If no MarshalJSON method is present but the value implements encoding.TextMarshaler instead, Marshal calls its MarshalText method and encodes the result as a JSON string.

The json/encoding package doesn't care about the String() method. So if you want to control the JSON representation / output of your value (the Ticket struct), implement json.Marshaler on it (in which you may call String() to your liking):

func (t Ticket) MarshalJSON() ([]byte, error) {
    return []byte(`"` + t.String() + `"`), nil
}

Then the output will be as you desire:

{"Name":"John","Ticket":"New York - Washington"}

Try it on the Go Playground.

One thing to look out for: if the string produced by Ticket.String() would contain a quotation mark ", the output would become invalid JSON, or more likely json.Marshal() would return an error.

To take care of such escaping, best / easiest is to use the json package itself: tell it to JSON-encode the string result of Ticket.String():

func (t Ticket) MarshalJSON() ([]byte, error) {
    return json.Marshal(t.String())
}

Now if we test it like this:

p.Name = "John"
p.Tkt.From = "New\" York" // CONTAINS QUOTE
p.Tkt.To = "Washington"

buf, err := json.Marshal(p)
fmt.Println(string(buf), err)

The output will still be a valid JSON (try it on the Go Playground):

{"Name":"John","Ticket":"New\" York - Washington"} <nil>