Як парсити різнотипний JSON в Go

Автор або джерело: Ярослав Характерник

Першоджерело

Go JSON, tech,

Повна версія

Як парсити різнотипний JSON в Go

Підписуйтеся наTelegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт, тему статті простіше буде одразу пояснити на прикладах коду.

Чи просто в Go розпарсити JSON {"name":"golang/go", "stars":84300}? Так.
А [{"name":"golang/go", "stars":84300}]? Так.
А ["golang/go", 84300]? Складніше.
А [["golang/go", 84300], ["rust-lang/rust", 53700]]? Ще складніше.
Тема парсингу JSON розрахована на гоферів, які перекваліфікувались з JavaScript, PHP, Python чи Ruby. Буде корисна досвідченим гоферам, щоб знати, що детальне пояснення вже є та рекомендувати новеньким в команді. У статті буде багато прикладів коду та детальні пояснення з посиланнями на популярні opensource-рішення.

Буденність JSON-у

Працюючи з PHP та JavaScript, я звик, що можу легко розпарсити будь-який валідний JSON. Працюючи з Go легко розпарсити JSON, який відповідає статичнотипізованому формату:

{
  "name": "json-unmarshal-research",
  "version": "1.0.0"
}


package main

import (
    "encoding/json"
    "github.com/stretchr/testify/require"
    "testing"
)

func TestPackageUnmarshalJSON(t *testing.T) {
    const (
        // language=JSON
        content = `{
  "name": "json-unmarshal-research",
  "version": "1.0.0"
}`
    )

    type Package struct {
        Name    string `json:"name"`
        Version string `json:"version"`
    }

    var (
        actual   Package
        expected = Package{
            Name:    "json-unmarshal-research",
            Version: "1.0.0",
        }
    )

    var err = json.Unmarshal([]byte(content), &actual)

    require.NoError(t, err)
    require.Equal(t, expected, actual)
}

Або більший приклад теж легко розпарсити в Go:

[
  {
    "id": 724712,
    "name": "rust",
    "full_name": "rust-lang/rust",
    "description": "Empowering everyone to build reliable and efficient software.",
    "created_at": "2010-06-16T20:39:03Z",
    "updated_at": "2021-04-15T14:54:00Z",
    "homepage": "https://www.rust-lang.org",
    "size": 616241,
    "stargazers_count": 53889,
    "language": "Rust",
    "has_wiki": false,
    "archived": false,
    "forks": 7753,
    "open_issues": 7120,
    "watchers": 53889,
    "default_branch": "master",
    "organization": {
      "login": "rust-lang",
      "id": 5430905
    },
    "network_count": 7753,
    "subscribers_count": 1497
  },
  {
    "id": 23096959,
    "name": "go",
    "full_name": "golang/go",
    "description": "The Go programming language",
    "created_at": "2014-08-19T04:33:40Z",
    "updated_at": "2021-04-15T14:59:43Z",
    "homepage": "https://golang.org",
    "size": 257437,
    "stargazers_count": 84507,
    "language": "Go",
    "has_wiki": true,
    "archived": false,
    "forks": 12312,
    "open_issues": 7045,
    "watchers": 84507,
    "default_branch": "master",
    "organization": {
      "login": "golang",
      "id": 4314092
    },
    "network_count": 12312,
    "subscribers_count": 3530
  },
  {
    "id": 44838949,
    "name": "swift",
    "full_name": "apple/swift",
    "description": "The Swift Programming Language",
    "created_at": "2015-10-23T21:15:07Z",
    "updated_at": "2021-04-15T15:00:43Z",
    "homepage": "https://swift.org",
    "size": 661779,
    "stargazers_count": 55730,
    "language": "C++",
    "has_wiki": false,
    "archived": false,
    "forks": 8947,
    "open_issues": 352,
    "watchers": 55730,
    "default_branch": "main",
    "organization": {
      "login": "apple",
      "id": 10639145
    },
    "network_count": 8947,
    "subscribers_count": 2589
  }
]

Для цього вставте JSON в онлайн-інструмент JSON to Go, який робить рутинну роботу за нас:

type AutoGenerated []struct {
    ID               int          `json:"id"`
    Name             string       `json:"name"`
    FullName         string       `json:"full_name"`
    Description      string       `json:"description"`
    CreatedAt        time.Time    `json:"created_at"`
    UpdatedAt        time.Time    `json:"updated_at"`
    Homepage         string       `json:"homepage"`
    Size             int          `json:"size"`
    StargazersCount  int          `json:"stargazers_count"`
    Language         string       `json:"language"`
    HasWiki          bool         `json:"has_wiki"`
    Archived         bool         `json:"archived"`
    Forks            int          `json:"forks"`
    OpenIssues       int          `json:"open_issues"`
    Watchers         int          `json:"watchers"`
    DefaultBranch    string       `json:"default_branch"`
    Organization     Organization `json:"organization"`
    NetworkCount     int          `json:"network_count"`
    SubscribersCount int          `json:"subscribers_count"`
}

type Organization struct {
    Login string `json:"login"`
    ID    int    `json:"id"`
}


type Repository struct {
    ID               int          `json:"id"`
    // ...
    Organization     Organization `json:"organization"`
    // ..
}

var repositories []Repository

var unmarshalErr = json.Unmarshal([]byte(content), &repositories)

require.NoError(t, unmarshalErr)
require.Equal(t, 3, len(repositories))

t.Log(repositories)

А от складності будемо розглядати в контексті задач.

Різнотипні повідомлення в вебсокетах

Є великий, але простий приклад вебсокету, зверніть увагу на змінну message , яка містить масив байтів.

package main

import (
    "fmt"
    "github.com/gorilla/websocket"
    "net/http"
)

func main() {
    var upgrader = websocket.Upgrader{
        ReadBufferSize:  1024,
        WriteBufferSize: 1024,
    }

    http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
        conn, upgradeErr := upgrader.Upgrade(w, r, nil)
        if upgradeErr != nil {
            fmt.Printf("[ERROR] WebSocket upgrade %+v\n", upgradeErr)

            // NOP

            return
        }

        for {
            var (
                messageType    int
                message        []byte
                readMessageErr error
            )

            // Read message from browser
            messageType, message, readMessageErr = conn.ReadMessage()
            if readMessageErr != nil {
                fmt.Printf("[ERROR] WebSocket read message %+v\n", readMessageErr)

                // NOP

                return
            }

            // Print the message to the console
            fmt.Printf("[INFO] WebSocket %s sent: %s\n", conn.RemoteAddr(), message)

            // Write message back to browser
            writeMessageErr := conn.WriteMessage(messageType, message)
            if writeMessageErr != nil {
                fmt.Printf("[ERROR] WebSocket write message %+v\n", writeMessageErr)

                // NOP

                return
            }
        }
    })

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, "index.html")
    })

    fmt.Println("[INFO] start server on port 8080")

    var serveErr = http.ListenAndServe(":8080", nil)
    if serveErr != nil {
        fmt.Printf("[ERROR] serve %+v\n", serveErr)
    }
}



<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Go WebSocket JSON</title>
</head>
<body>
<div>
    <input id="js-input" type="text"/>
    <button onclick="send()">Send</button>
</div>
<pre id="js-output"></pre>
<script>
    const $input = document.getElementById("js-input");
    const $output = document.getElementById("js-output");
    const socket = new WebSocket("ws://localhost:8080/echo");

    socket.onopen = function () {
        $output.innerHTML += "Status: Connected\n";
    };

    socket.onmessage = function (e) {
        $output.innerHTML += "Server: " + e.data + "\n";
    };

    function send() {
        socket.send($input.value);
        $input.value = "";
    }
</script>
</body>
</html>

Тепер будемо в message передавати різнотипний JSON щоб розпарсити в Go:

const socket = new WebSocket("ws://localhost:8080/echo");

socket.onopen = function () {
    socket.send(JSON.stringify({
        "command_name": "employer_add_company",
        "data": {
            "name": "Example",
            "urls": ["https://example.com"]
        }
    }));

    socket.send(JSON.stringify({
        "command_name": "employer_add_project",
        "data": {
            "company_id": 1,
            "name": "Example",
            "urls": ["https://example.com"],
            "domains": ["EdTech", "HealthTech", "FoodTech"]
        }
    }));

    socket.send(JSON.stringify({
        "command_name": "employer_add_vacancy",
        "data": {
            "company_id": 1,
            "name": "Senior Rust Developer",
            "description": "Example description",
            "parking": {
                "exists": false,
                "free": false,
            },
            "remote": true
        }
    }));
}

Розглянемо різні варіанти парсингу від простого до складного, альтернативний варіант, а також латки.

Найзрозуміліший варіант — це розпарсити назву команди та в залежності від назви все повідомлення.

Парсинг відбувається в методі Dispatch :

type Company struct {
    Name string   `json:"name"`
    Urls []string `json:"urls"`
}

type Project struct {
    CompanyID int      `json:"company_id"`
    Name      string   `json:"name"`
    Urls      []string `json:"urls"`
    Domains   []string `json:"domains"`
}

type Vacancy struct {
    CompanyID   int     `json:"company_id"`
    Name        string  `json:"name"`
    Description string  `json:"description"`
    Parking     Parking `json:"parking"`
    Remote      bool    `json:"remote"`
}

type Parking struct {
    Exists bool `json:"exists"`
    Free   bool `json:"free"`
}

type Command struct {
    Name string `json:"command_name"`
}

type EmployerAddCompanyCommand struct {
    CommandName string  `json:"command_name"` // unused, @TODO delete
    Data        Company `json:"data"`
}

type EmployerAddProjectCommand struct {
    CommandName string  `json:"command_name"` // unused, @TODO delete
    Data        Project `json:"data"`
}

type EmployerAddVacancyCommand struct {
    CommandName string  `json:"command_name"` // unused, @TODO delete
    Data        Vacancy `json:"data"`
}


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

type Session struct {
    companies []Company
    projects  []Project
    vacancies []Vacancy
}

func (s *Session) Dispatch(raw []byte) error {
    var command Command

    var nameErr = json.Unmarshal(raw, &command)
    if nameErr != nil {
        return nameErr
    }

    switch command.Name {
    case "employer_add_company":
        var wrapper EmployerAddCompanyCommand

        var dataErr = json.Unmarshal(raw, &wrapper)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchCompany(wrapper.Data)

        return nil
    case "employer_add_project":
        var wrapper EmployerAddProjectCommand

        var dataErr = json.Unmarshal(raw, &wrapper)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchProject(wrapper.Data)

        return nil
    case "employer_add_vacancy":
        var wrapper EmployerAddVacancyCommand

        var dataErr = json.Unmarshal(raw, &wrapper)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchVacancy(wrapper.Data)

        return nil
    }

    return errors.New(fmt.Sprintf("undefined command %s", command.Name))
}

func (s *Session) DispatchCompany(c Company) {
    s.companies = append(s.companies, c)
}

func (s *Session) DispatchProject(c Project) {
    s.projects = append(s.projects, c)
}

func (s *Session) DispatchVacancy(v Vacancy) {
    s.vacancies = append(s.vacancies, v)
}


import (
    "github.com/stretchr/testify/require"
    "testing"
)

func TestSession_Dispatch(t *testing.T) {
    const (
        // language=JSON
        addCompanyJSON = `{
    "command_name": "employer_add_company",
    "data": {
        "name": "Example",
        "urls": ["https://example.com"]
    }
}`
        // language=JSON
        addProjectJSON = `{
    "command_name": "employer_add_project",
    "data": {
        "company_id": 1,
        "name": "Example",
        "urls": ["https://example.com"],
        "domains": ["EdTech", "HealthTech", "FoodTech"]
    }
}`
        // language=JSON
        addVacancyJSON = `{
    "command_name": "employer_add_vacancy",
    "data": {
        "company_id": 1,
        "name": "Senior Rust Developer",
        "description": "Example description",
        "parking": {
            "exists": false,
            "free": false
        },
        "remote": true
    }
}`
    )

    {
        var session Session

        var err = session.Dispatch([]byte(addCompanyJSON))

        require.NoError(t, err)

        require.Equal(t, Session{
            companies: []Company{
                {
                    Name: "Example",
                    Urls: []string{"https://example.com"},
                },
            },
        }, session)
    }

    {
        var session Session

        var err = session.Dispatch([]byte(addProjectJSON))

        require.NoError(t, err)

        require.Equal(t, Session{
            projects: []Project{
                {
                    CompanyID: 1,
                    Name:      "Example",
                    Urls:      []string{"https://example.com"},
                    Domains:   []string{"EdTech", "HealthTech", "FoodTech"},
                },
            },
        }, session)
    }

    {
        var session Session

        var err = session.Dispatch([]byte(addVacancyJSON))

        require.NoError(t, err)

        require.Equal(t, Session{
            vacancies: []Vacancy{
                {
                    CompanyID:   1,
                    Name:        "Senior Rust Developer",
                    Description: "Example description",
                    Parking: Parking{
                        Exists: false,
                        Free:   false,
                    },
                    Remote: true,
                },
            },
        }, session)
    }
}

Схожий варіант — це розпарсити поле command_name , використовуючи github.com/valyala/fastjson :

func (s *Session) Dispatch(raw []byte) error {
    var commandName = fastjson.GetString(raw, "command_name")

    switch commandName {
    case "employer_add_company":
        // as before example Company
        // ...

        return nil
    case "employer_add_project":
        // as before example Project
        // ...

        return nil
    case "employer_add_vacancy":
        // as before example Vacancy
        // ...

        return nil
    }

    return errors.New(fmt.Sprintf("undefined command %s", commandName))
}

І третій варіант — використовувати json.RawMessage зі стандартної бібліотеки encoding/json для відкладеного парсингу (або відкладеної серіалізації):

// RawMessage is a raw encoded JSON value.
// It implements Marshaler and Unmarshaler and can
// be used to delay JSON decoding or precompute a JSON encoding.
type RawMessage []byte

// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
    if m == nil {
        return []byte("null"), nil
    }
    return m, nil
}

// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
    if m == nil {
        return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
    }
    *m = append((*m)[0:0], data...)
    return nil
}


type CommandRawData struct {
    Name string          `json:"command_name"`
    Data json.RawMessage `json:"data"`
}


func (s *Session) Dispatch(raw []byte) error {
    var command CommandRawData

    var nameErr = json.Unmarshal(raw, &command)
    if nameErr != nil {
        return nameErr
    }

    switch command.Name {
    case "employer_add_company":
        var company Company

        var dataErr = json.Unmarshal(command.Data, &company)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchCompany(company)

        return nil
    case "employer_add_project":
        var project Project

        var dataErr = json.Unmarshal(command.Data, &project)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchProject(project)

        return nil
    case "employer_add_vacancy":
        var vacancy Vacancy

        var dataErr = json.Unmarshal(command.Data, &vacancy)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchVacancy(vacancy)

        return nil
    }

    return errors.New(fmt.Sprintf("undefined command %s", command.Name))
}

Ось додатковий приклад, щоб краще зрозуміти, як працює json.RawMessage :

import (
    "encoding/json"
    "github.com/stretchr/testify/require"
    "testing"
)

func TestSession_RawMessageUnmarshal(t *testing.T) {
    type CommandRawData struct {
        Name string          `json:"command_name"`
        Data json.RawMessage `json:"data"`
    }

    const (
        // language=JSON
        addCompanyJSON = `{"command_name":"any","data":{"name":"Example","urls":["https://example.com"]}}`
    )

    var (
        actual   CommandRawData
        expected = CommandRawData{
            Name: "any",
            Data: json.RawMessage([]byte(`{"name":"Example","urls":["https://example.com"]}`)),
        }
    )

    var err = json.Unmarshal([]byte(addCompanyJSON), &actual)

    require.NoError(t, err)
    require.Equal(t, expected, actual)
}

Основні варіанти розглянули, ще залишився альтернативний варіант та латки.

Парсинг JSON-у в месенджері Tinode

Tinode — це месенджер, бекенд якого написаний на чистому Go, а код є у відкритому доступі github.com/tinode/chat.
Tinode також використовує вебсокети, а тому цікаво глянути, як парсять повідомлення:

// ClientComMessage is a wrapper for client messages.
type ClientComMessage struct {
    Hi    *MsgClientHi    `json:"hi"`
    Acc   *MsgClientAcc   `json:"acc"`
    Login *MsgClientLogin `json:"login"`
    Sub   *MsgClientSub   `json:"sub"`
    Leave *MsgClientLeave `json:"leave"`
    Pub   *MsgClientPub   `json:"pub"`
    Get   *MsgClientGet   `json:"get"`
    Set   *MsgClientSet   `json:"set"`
    Del   *MsgClientDel   `json:"del"`
    Note  *MsgClientNote  `json:"note"`

    // ...
}


func (s *Session) dispatchRaw(raw []byte) {
    var msg ClientComMessage

    if err := json.Unmarshal(raw, &msg); err != nil {
        // ...

        return
    }

    s.dispatch(&msg)
}


func (s *Session) dispatch(msg *ClientComMessage) {
    switch {
    case msg.Pub != nil:
        // ...

    case msg.Sub != nil:
        // ...

    case msg.Leave != nil:
        // ...

    case msg.Hi != nil:
        // ...

    case msg.Login != nil:
        // ...

    case msg.Get != nil:
        // ...

    case msg.Set != nil:
        // ...

    case msg.Del != nil:
        // ...

    case msg.Acc != nil:
        // ...

    case msg.Note != nil:
        // ...

    default:
        // ...

        return
    }
}

Для кожного повідомлення власний ключ.

Будемо вважати за четвертий варіант парсингу різнотипного JSON-у, комфортний для малого числа типів повідомлень. У Tinode їх всього 10.

Універсальна подвійна серіалізація

У мережі мало статей з прикладами, як парсити різнотипний JSON, а тому початківці в Go знаходять найпростіший варіант: додатково серіалізувати повідомлення на стороні клієнта (щоб простіше було парсити на сервері):

const socket = new WebSocket("ws://localhost:8080/echo");

socket.onopen = function () {
    socket.send(JSON.stringify({
        "command_name": "employer_add_company",
        "data": JSON.stringify({
            "name": "Example",
            "urls": ["https://example.com"]
        })
    }));

    socket.send(JSON.stringify({
        "command_name": "employer_add_project",
        "data": JSON.stringify({
            "company_id": 1,
            "name": "Example",
            "urls": ["https://example.com"],
            "domains": ["EdTech", "HealthTech", "FoodTech"]
        })
    }));

    socket.send(JSON.stringify({
        "command_name": "employer_add_vacancy",
        "data": JSON.stringify({
            "company_id": 1,
            "name": "Senior Rust Developer",
            "description": "Example description",
            "parking": {
                "exists": false,
                "free": false
            },
            "remote": true
        })
    }));
}


type CommandStringData struct {
    Name string `json:"command_name"`
    Data string `json:"data"`
}


func (s *Session) Dispatch(raw []byte) error {
    var command CommandStringData

    var nameErr = json.Unmarshal(raw, &command)
    if nameErr != nil {
        return nameErr
    }

    switch command.Name {
    case "employer_add_company":
        var company Company

        var dataErr = json.Unmarshal([]byte(command.Data), &company)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchCompany(company)

        return nil
    case "employer_add_project":
        var project Project

        var dataErr = json.Unmarshal([]byte(command.Data), &project)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchProject(project)

        return nil
    case "employer_add_vacancy":
        var vacancy Vacancy

        var dataErr = json.Unmarshal([]byte(command.Data), &vacancy)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchVacancy(vacancy)

        return nil
    }

    return errors.New(fmt.Sprintf("undefined command %s", command.Name))
}


func TestSession_Dispatch(t *testing.T) {
    const (
        // language=JSON
        addCompanyJSON = `{
    "command_name": "employer_add_company",
    "data": "{\"name\":\"Example\",\"urls\":[\"https://example.com\"]}"
}`
    )

    {
        var session Session

        var err = session.DispatchStringData([]byte(addCompanyJSON))

        require.NoError(t, err)

        require.Equal(t, Session{
            companies: []Company{
                {
                    Name: "Example",
                    Urls: []string{"https://example.com"},
                },
            },
        }, session)
    }
}

Варіант робочий, але через подвійну серіалізацію складніше читати у веб- консолі, попередні варіанти кращі.

Безключовий JSON у вебсокет повідомленнях

Ще один формат, який зустрічається у вебсокетах, економний-безключовий:

const socket = new WebSocket("ws://localhost:8080/echo");

socket.onopen = function () {
    socket.send(JSON.stringify([
        "employer_add_company",
        {
            "name": "Example",
            "urls": ["https://example.com"]
        }
    ]));

    socket.send(JSON.stringify([
        "employer_add_project",
        {
            "company_id": 1,
            "name": "Example",
            "urls": ["https://example.com"],
            "domains": ["EdTech", "HealthTech", "FoodTech"]
        }
    ]));

    socket.send(JSON.stringify([
        "employer_add_vacancy",
        {
            "company_id": 1,
            "name": "Senior Rust Developer",
            "description": "Example description",
            "parking": {
                "exists": false,
                "free": false,
            },
            "remote": true
        }
    ]));
}

Для парсингу різнотипного масиву підходить []json.RawMessage :

func (s *Session) Dispatch(raw []byte) error {
    var command [2]json.RawMessage

    var err = json.Unmarshal(raw, &command)
    if err != nil {
        return err
    }

    switch string(command[0]) {
    case `"employer_add_company"`:
        var company Company

        var dataErr = json.Unmarshal(command[1], &company)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchCompany(company)

        return nil
    case `"employer_add_project"`:
        var project Project

        var dataErr = json.Unmarshal(command[1], &project)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchProject(project)

        return nil
    case `"employer_add_vacancy"`:
        var vacancy Vacancy

        var dataErr = json.Unmarshal(command[1], &vacancy)
        if dataErr != nil {
            return dataErr
        }

        s.DispatchVacancy(vacancy)

        return nil
    }

    return errors.New(fmt.Sprintf("undefined command %s", string(command[0])))
}


func TestSession_Dispatch(t *testing.T) {
    const (
        // language=JSON
        addCompanyJSON = `[
    "employer_add_company",
    {
        "name": "Example",
        "urls": ["https://example.com"]
    }
]`
        // language=JSON
        addCompanyThirdKeyJSON = `[
    "employer_add_company",
    {
        "name": "Example",
        "urls": ["https://example.com"]
    },
    "test third key"
]`
    )

    var expected = Session{
        companies: []Company{
            {
                Name: "Example",
                Urls: []string{"https://example.com"},
            },
        },
    }

    {
        var session Session

        var err = session.Dispatch([]byte(addCompanyJSON))

        require.NoError(t, err)

        require.Equal(t, expected, session)
    }

    {
        var session Session

        var err = session.Dispatch([]byte(addCompanyThirdKeyJSON))

        require.NoError(t, err)

        require.Equal(t, expected, session)
    }
}

Зверніть увагу, раніше в switch порівнював з employer_add_company , а зараз з " "employer_add_company".

Я використав [2]json.RawMessage , а якщо використовувати слайс []json.RawMessage , то треба додатково перевіряти розмір.

func (s *Session) Dispatch(raw []byte) error {
    var command []json.RawMessage

    var err = json.Unmarshal(raw, &command)
    if err != nil {
        return err
    }

    if len(command) < 2 {
        return errors.New(fmt.Sprintf("invalid json format"))
    }

    var (
        commandName string
        nameErr     = json.Unmarshal(command[0], &commandName)
    )
    if nameErr != nil {
        return nameErr
    }

    switch commandName {
    case "employer_add_company":
        // as before example
        // ...

        return nil
    case "employer_add_project":
        // as before example
        // ...

        return nil
    case "employer_add_vacancy":
        // as before example
        // ...

        return nil
    }

    return errors.New(fmt.Sprintf("undefined command %s", commandName))
}

Епілог та далі буде

У статті я описав основні ідеї, які допоможуть парсити різнотипні формати JSON-у. У процесі написання статті розписав заголовки, про що хочу написати, як інтерфейс. Ще залишилось сім заголовків, а тому у теми парсингу JSON-у буде продовження.