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

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

Чи просто в 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"
	"testing"

	"github.com/stretchr/testify/require"
)

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"
	"net/http"

	"github.com/gorilla/websocket"
)

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 (
	"testing"

	"github.com/stretchr/testify/require"
)

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"
	"testing"

	"github.com/stretchr/testify/require"
)

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-у буде продовження.

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

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному4
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Все одно продовжу користуватись mholt.github.io/json-to-go бо робить перетворення в браузері.

А jsonformatter.org/json-to-go робить перетворення на сервері.

А як же кодогенерація? Через неї можна також парсити різні джейсони.

Про кодогенерацію з easyjson писав раніше у статті Protobuf vs JSON.
А яку ви використовуєте в проекті?

Не побачив, як і в тій статті — easyJson

Ручний імперативний парсинг?
Може краще JSON Schema використовувати?

Повністю декларативний опис складних структур, автоматична їх валідація та часто парсери реалізують повний парсинг без участі людини.
Попробуйте.

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

JSON Schema малопопулярна в Go спільноті:

  1. github.com/xeipuuv/gojsonschema 1800+ зірок
  2. github.com/faceair/jio 55 зірок
  3. github.com/jgroeneveld/schema 15 зірок

Приклад з joi.dev/api для розуміння JSON Schema:

const Joi = require('joi');

const schema = Joi.object({
    username: Joi.string()
        .alphanum()
        .min(3)
        .max(30)
        .required(),

    password: Joi.string()
        .pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),

    repeat_password: Joi.ref('password'),

    access_token: [
        Joi.string(),
        Joi.number()
    ],

    birth_year: Joi.number()
        .integer()
        .min(1900)
        .max(2013),

    email: Joi.string()
        .email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
})
    .with('username', 'birth_year')
    .xor('password', 'access_token')
    .with('password', 'repeat_password');


schema.validate({ username: 'abc', birth_year: 1994 });
// -> { value: { username: 'abc', birth_year: 1994 } }

schema.validate({});
// -> { value: {}, error: '"username" is required' }

Якщо у вас є гарні приклади з використанням JSON Schema то поділіться бо на мою думку в Go це ускладнення коду для взаємодії через API та сокети.

Топовая статья, спасибо!

а чому нема через map[string]interface{} ?

В мережі повно прикладів з map[string]interface{}: Go за Прикладом: JSON.

Повно прикладів в стандартній бібліотеці encoding/json decode_test.go:

var ifaceNumAsFloat64 = map[string]interface{}{
	"k1": float64(1),
	"k2": "s",
	"k3": []interface{}{float64(1), float64(2.0), float64(3e-3)},
	"k4": map[string]interface{}{"kk1": "s", "kk2": float64(2)},
}

var ifaceNumAsNumber = map[string]interface{}{
	"k1": Number("1"),
	"k2": "s",
	"k3": []interface{}{Number("1"), Number("2.0"), Number("3e-3")},
	"k4": map[string]interface{}{"kk1": "s", "kk2": Number("2")},
}

Буде продовження статті з interface{} та map[string]interface{} на прикладі відповіді від PHP API

В JSON кстати есть один конструктивный недочёт: нет никакой возможности указания типа, и поэтому поля int ничем синтаксически не отличаются от float32/float64. Таким образом, если парсить объект в структуру типа map[string]interface{}, все числовые значения будут float вместо int.

Не сказал бы что это недочёт. Таков формат. Парси стринги и будет тебе счастье. JSON служит для переброски данных скорее чем для прототипирования оных в классы. Потому что там где классы, там подавай и наследования, и кошерные именования... короче это будет уже совсем не JSON.

Хочешь лояльно относиться к JSONу — посмотри что в XML у корпоративных Microsoft поделий, ты на этот JSON молиться будешь.

В YAML как раз те самые проблемы JSON решены, там не только можно при желании указывать директивно тип, но даже стримить ссылки. И ещё каменты можно писать, а этого очень не хватает в конфигах в json.

Это всё самопальные расширения, не поддержанные стандартом, а значит с узким кругом применения. С тем же успехом можно вообще не в JSON стримить.

Я так и сказал. Можно много чего допилить, но тогда это будет уже не JavaScript Object Notation. JSON задумывался под примитивную задачу — отдать его eval на JS. Разумеется, всё пошло через жопу сразу же, как только пошла передача между разными устройствами — всё что угодно могло прилететь в этот eval и отработать на ура, особенно в Ослике. Убытки от этого дерьма давно уж превысили в добрую сотню раз потери во всех войнах за всю историю. Такова цена простоты.

Лайк, JSON-to-Go открыт мною давно, очень полезный инструмент для работы с массивными API, сэкономил мне тонну времени.

я тут пошел почитал release notes к GoLand 2021.1, и оказалось что это уже встроили даже в IDE: www.jetbrains.com/...​te-json-in-the-editor.gif

Може знаєш за допомогою якого інструменту роблять ці гіфки?

неожиданно Go встроили в саблайм и тоже никому ничего не сказали:))) еще пару лет назад

Підписатись на коментарі