Unmarshal JSON динамически в структуры, содержащие только поля верхнего уровня, основанные на поле типа - PullRequest
0 голосов
/ 12 апреля 2020

Скажем, у меня есть следующие структуры событий, которые я не могу изменить (даже теги):

type openAudioStream struct {
    Type     string `json:"type"`
    ID       string `json:"id"`
    Address  string `json:"address"`
    Channels int    `json:"channels"`
}

type openVideoStream struct {
    Type        string `json:"type"`
    ID          string `json:"id"`
    Address     string `json:"address"`
    AspectRatio string `json:"aspectRatio"`
}

И у меня есть конечная точка API, которая генерирует строки JSON (которые я также не могу изменить), содержащие события, которые отображаются на одну из этих двух структур, и я не могу заранее сказать, какая это, поэтому мне нужно каким-то образом извлечь поле типа, чтобы выяснить, какую структуру нужно создать, а затем разархивировать оставшуюся часть JSON в экземпляр объекта события.

Первый подход, который мне пришёл в голову, - это вызвать json.Unmarshal дважды, например:

func jsonUnmarshal(payload []byte) (Event, error) {
    eventType := struct {
        Type string
    }{}

    err := json.Unmarshal(payload, &eventType)
    if err != nil {
        return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
    }

    var event Event
    switch eventType.Type {
    case "audio":
        event = &openAudioStream{}
        err = json.Unmarshal(payload, event)
    case "video":
        event = &openVideoStream{}
        err = json.Unmarshal(payload, event)
    default:
        err = fmt.Errorf("unrecognised event type: %s", eventType.Type)
    }

    if err != nil {
        return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
    }

    return event, nil
}

Хотя это работает хорошо, неэффективно обходить JSON string дважды, так что я подумал, что, может быть, я могу создать тип объединения и использовать его для демонтажа JSON, например, так:

func notWorking(payload []byte) (Event, error) {
    eventUnion := struct {
        Type string `json:"type"`
        openAudioStream
        openVideoStream
    }{}

    err := json.Unmarshal(payload, &eventUnion)
    if err != nil {
        return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
    }

    var event Event
    switch eventUnion.Type {
    case "audio":
        event = &extractor.openAudioStream
    case "video":
        event = &extractor.openVideoStream
    default:
        return nil, fmt.Errorf("unrecognised event type: %s", eventUnion.Type)
    }

    return event, nil
}

Помимо того, что этот уродливый хак, этот подход не работает если встроенные структуры содержат конфликтующие поля. json unmarshaler просто игнорирует их без каких-либо ошибок.

Наконец, я вспомнил, что mapstructure может помочь, и, действительно, я могу использовать его как так:

func mapstructureDecode(payload []byte) (Event, error) {
    var unmarshaledPayload map[string]interface{}

    err := json.Unmarshal(payload, &unmarshaledPayload)
    if err != nil {
        return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
    }

    var ok bool
    var val interface{}
    var eventType string
    if val, ok = unmarshaledPayload["type"]; ok {
        eventType, ok = val.(string)
    }
    if !ok {
        return nil, fmt.Errorf("failed to determine event type: %v", err)
    }

    var event Event
    switch eventType {
    case "audio":
        event = &openAudioStream{}
        err = mapstructure.Decode(unmarshaledPayload, &event)
    case "video":
        event = &openVideoStream{}
        err = mapstructure.Decode(unmarshaledPayload, event)
    default:
        err = fmt.Errorf("unrecognised event type: %s", eventType)
    }

    if err != nil {
        return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
    }

    return event, nil
}

Однако использование этой библиотеки выглядит несколько излишним для этой задачи, и для нее требуется добавление тегов mapstructure в поля структуры, если входные данные JSON не соответствуют стандартным соглашениям об именах, что является проблемой, если у меня есть, например, aspect_ratio вместо aspectRatio.

Полный код для вышеуказанных экспериментов можно найти здесь: https://play.golang.org/p/qTGoV6i8m5P

Мне любопытно, есть ли другой способ решения этой проблемы с использованием существующих библиотек. Я думал, что, возможно, какое-то творческое использование json.RawMessage и пользовательского метода UnmarshalJSON могло бы помочь, но это, кажется, бесполезно, если в структурах событий есть только поля верхнего уровня.

1 Ответ

0 голосов
/ 12 апреля 2020

Вы можете разархивировать один раз в объединенной структуре, а затем создать новую структуру в зависимости от типа.


func jsonUnmarshal(payload []byte) (Event, error) {
    data := struct {
        Type        string
        ID          string
        Address     string
        Channels    int
        AspectRatio string
    }{}
    // Unmarshal in combined stuct
    err := json.Unmarshal(payload, &data)
    if err != nil {
        return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
    }
    var event Event
    switch data.Type {
    case "audio":
        // Creating new stuct 
        event = &openAudioStream{Type: data.Type, ID: data.ID, Address: data.Address, Channels: data.Channels}
    case "video":
        event = &openVideoStream{Type: data.Type, ID: data.ID, Address: data.Address, AspectRatio: data.AspectRatio}
    default:
        err = fmt.Errorf("unrecognised event type: %s", data.Type)
    }
    if err != nil {
        return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
    }
    return event, nil
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...