Go에서 JSON 가독성 향상

30312 단어 gojson
화면의 한 줄에 맞는 JSON 문서는 일반적으로 읽기 쉽지만 문서가 커지고 수평 스크롤이 필요하면 이해하기가 훨씬 어려워집니다.

{"foo":"bar"}


들여쓰기는 왼쪽 패딩이 있는 별도의 줄에 구문 단위를 배치하여 사람이 읽을 수 있도록 JSON 형식을 개선하는 데 도움이 됩니다.

[
  {
    "comment": "empty list, empty docs",
    "doc": {},
    "patch": [],
    "expected": {}
  },
  {
    "comment": "empty patch list",
    "doc": {
      "foo": 1
    },
    "patch": [],
    "expected": {
      "foo": 1
    }
  }
]


평균 크기의 문서에서는 잘 작동하지만 계층이 풍부한 큰 문서에서는 불편한 경향이 있습니다. 더 큰 문서는 너무 많은 줄을 사용하여 렌더링되며 읽기 위해 상당한 수직 스크롤이 필요합니다. 작은 요소라도 줄을 긋기 때문에 종종 화면 공간의 작은 부분만 데이터에 활용됩니다.

더 작은 조각은 압축되고 더 큰 조각은 들여쓰기될 때 하이브리드 접근 방식으로 타협에 도달할 수 있습니다.

{
  "openapi":"3.0.2",
  "info":{"title":"","version":""},
  "paths":{
    "/test/{in-path}":{
      "post":{
        "summary":"Title",
        "description":"",
        "operationId":"name",
        "parameters":[
          {"name":"in_query","in":"query","schema":{"type":"integer"}},
          {"name":"in-path","in":"path","required":true,"schema":{"type":"boolean"}},
          {"name":"in_cookie","in":"cookie","schema":{"type":"number"}},
          {"name":"X-In-Header","in":"header","schema":{"type":"string"}}
        ],
        "requestBody":{
          "content":{
            "application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/FormDataOpenapiTestInput"}}
          }
        },
        "responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}}},
        "deprecated":true
      }
    }
  },
  "components":{
    "schemas":{"FormDataOpenapiTestInput":{"type":"object","properties":{"in_form_data":{"type":"string"}}}}
  }
}


Go에서 이러한 마샬러를 구현해 봅시다. JSON 문서를 살펴보고 이미 작은 잎을 찾을 때까지 들여쓰기를 적용할 수 있습니다. 그런 다음 해당 잎을 컴팩트한 형태로 렌더링할 수 있습니다.

문서 너비 제한으로 줄 길이를 사용할 수 있습니다. 패딩된 JSON 라인이 라인 길이 제한보다 짧으면 압축 형식을 사용할 수 있습니다.

JSON 사양은 속성 순서에 대한 의미 체계를 정의하지 않지만 가독성을 위해 원래 순서를 유지하는 것이 중요합니다. 이를 위해 github.com/iancoleman/orderedmap을 사용할 수 있습니다.

// MarshalIndentCompact applies indentation for large chunks of JSON and uses compact format for smaller ones.
//
// Line length limits indented width of JSON structure, does not apply to long distinct scalars.
// This function is not optimized for performance, so it might be not a good fit for high load scenarios.
func MarshalIndentCompact(v interface{}, prefix, indent string, lineLen int) ([]byte, error) {
    b, err := json.Marshal(v)
    if err != nil {
        return nil, err
    }

    // Return early if document is small enough.
    if len(b) <= lineLen {
        return b, nil
    }

    m := orderedmap.New()

    // Create a temporary JSON object to make sure it can be unmarshaled into a map.
    tmpMap := append([]byte(`{"t":`), b...)
    tmpMap = append(tmpMap, '}')

    // Unmarshal JSON payload into ordered map to recursively walk the document.
    err = json.Unmarshal(tmpMap, m)
    if err != nil {
        return nil, err
    }

    i, ok := m.Get("t")
    if !ok {
        return nil, orderedmap.NoValueError
    }

    // Create first level padding.
    pad := append([]byte(prefix), []byte(indent)...)

    // Call recursive function to walk the document.
    return marshalIndentCompact(i, indent, pad, lineLen)
}


이제 marshalIndentCompact에서 type switch을 사용하여 배열과 객체에서 더 깊이 재귀할 수 있습니다.

func marshalIndentCompact(doc interface{}, indent string, pad []byte, lineLen int) ([]byte, error) {
    // Build compact JSON for provided sub document.
    compact, err := json.Marshal(doc)
    if err != nil {
        return nil, err
    }

    // Return compact if it fits line length limit with current padding.
    if len(compact)+len(pad) <= lineLen {
        return compact, nil
    }

    // Indent arrays and objects that are too big.
    switch o := doc.(type) {
    case orderedmap.OrderedMap:
        return marshalObject(o, len(compact), indent, pad, lineLen)
    case []interface{}:
        return marshalArray(o, len(compact), indent, pad, lineLen)
    }

    // Use compact for scalar values (numbers, strings, booleans, nulls).
    return compact, nil
}


배열은 []interface{}로 표시되며 모든 항목에 재귀 서식을 적용하기 위해 반복할 수 있습니다.

func marshalArray(o []interface{}, compactLen int, indent string, pad []byte, lineLen int) ([]byte, error) {
    // Allocate result with a size of compact form, because it is impossible to make result shorter.
    res := append(make([]byte, 0, compactLen), '[', '\n')

    for i, val := range o {
        // Build item value with an increased padding.
        jsonVal, err := marshalIndentCompact(val, indent, append(pad, []byte(indent)...), lineLen)
        if err != nil {
            return nil, err
        }

        // Add item JSON with current padding.
        res = append(res, pad...)
        res = append(res, jsonVal...)

        if i == len(o)-1 {
            // Close array at last item.
            res = append(res, '\n')
            // Strip one indent from a closing bracket.
            res = append(res, pad[len(indent):]...)
            res = append(res, ']')
        } else {
            // Add colon and new line after an item.
            res = append(res, ',', '\n')
        }
    }

    return res, nil
}


속성 순서를 유지하기 위해 순서가 지정된 키를 사용하여 반복할 수 있는 JSON 개체는 orderedmap.OrderedMap로 사용할 수 있습니다. 모든 속성 값은 들여쓰기를 통해 재귀적으로 처리됩니다.

func marshalObject(o orderedmap.OrderedMap, compactLen int, indent string, pad []byte, lineLen int) ([]byte, error) {
    // Allocate result with a size of compact form, because it is impossible to make result shorter.
    res := append(make([]byte, 0, compactLen), '{', '\n')

    // Iterate object using keys slice to preserve properties order.
    keys := o.Keys()
    for i, k := range keys {
        val, ok := o.Get(k)
        if !ok {
            return nil, orderedmap.NoValueError
        }

        // Build item value with an increased padding.
        jsonVal, err := marshalIndentCompact(val, indent, append(pad, []byte(indent)...), lineLen)
        if err != nil {
            return nil, err
        }

        // Marshal key as JSON string.
        kj, err := json.Marshal(k)
        if err != nil {
            return nil, err
        }

        // Add key JSON with current padding.
        res = append(res, pad...)
        res = append(res, kj...)
        res = append(res, ':')
        // Add value JSON to the same line.
        res = append(res, jsonVal...)

        if i == len(keys)-1 {
            // Close object at last property.
            res = append(res, '\n')
            // Strip one indent from a closing bracket.
            res = append(res, pad[len(indent):]...)
            res = append(res, '}')
        } else {
            // Add colon and new line after a property.
            res = append(res, ',', '\n')
        }
    }

    return res, nil
}


APIgithub.com/swaggest/assertjson 라이브러리와 CLI tool에서 사용할 수 있습니다. 테스트 및 기타 경우에 대한 JSON 가독성을 개선하는 데 도움이 되기를 바랍니다.

읽어 주셔서 감사합니다.

좋은 웹페이지 즐겨찾기