JSON

JSON

JSON은 JavaScript Object Notation(JSON) 데이터를 Ruby 객체로 파싱하고, Ruby 객체를 JSON 문자열로 생성하는 모듈이에요. 경량 데이터 교환 포맷답게, 문자열·숫자·불리언·null·배열·객체 여섯 가지 값만으로 구성돼요.

출처: Ruby 4.0 API

본문

JSON 값의 종류를 먼저 짚고 갈게요.

  • 큰따옴표로 감싼 텍스트: "foo".
  • 숫자: 1, 1.0, 2.0e2.
  • 불리언: true, false.
  • 널: null.
  • 배열: 대괄호로 감싼 값의 순서 목록:
["foo", 1, 1.0, 2.0e2, true, false, null]
  • 객체: 중괄호로 감싼 이름/값 쌍의 집합. 각 이름은 큰따옴표 텍스트고, 값은 어떤 JSON 값이든 될 수 있어요:
{"a": "foo", "b": 1, "c": 1.0, "d": 2.0e2, "e": true, "f": false, "g": null}

JSON 배열이나 객체는 어떤 깊이든 중첩된 배열·객체·스칼라를 담을 수 있어요.

{"foo": {"bar": 1, "baz": 2}, "bat": [0, 1, 2]}
[{"foo": 0, "bar": 1}, ["baz", 2]]

사용하려면 먼저 불러와요.

require 'json'

JSON 파싱하기

JSON 데이터를 담은 문자열은 두 메서드 중 하나로 파싱할 수 있어요: JSON.parse(source, opts)JSON.parse!(source, opts). source는 Ruby 객체, opts는 허용할 입력과 출력 포맷을 제어하는 옵션들의 해시예요.

두 메서드 차이는 이래요. JSON.parse!는 일부 검사를 생략해서 어떤 source 데이터에는 안전하지 않을 수 있어요. 신뢰할 수 있는 소스의 데이터에만 쓰세요. 덜 신뢰하는 소스에는 더 안전한 JSON.parse를 쓰는 게 좋아요.

배열 파싱

source가 JSON 배열이면 JSON.parse는 기본으로 Ruby Array를 돌려줘요.

json = '["foo", 1, 1.0, 2.0e2, true, false, null]'
ruby = JSON.parse(json)
ruby # => ["foo", 1, 1.0, 200.0, true, false, nil]
ruby.class # => Array

객체 파싱

source가 JSON 객체면 기본으로 Ruby Hash를 돌려줘요.

json = '{"a": "foo", "b": 1, "c": 1.0, "d": 2.0e2, "e": true, "f": false, "g": null}'
ruby = JSON.parse(json)
ruby # => {"a"=>"foo", "b"=>1, "c"=>1.0, "d"=>200.0, "e"=>true, "f"=>false, "g"=>nil}
ruby.class # => Hash

스칼라 파싱

source가 JSON 스칼라(배열·객체가 아닌 것)면 Ruby 스칼라를 돌려줘요. 문자열 '1.0'Float(200.0으로 2.0e2Float), '1'Integer, 'true'/'false'TrueClass/FalseClass, 'null'nil이 돼요.

파싱 옵션

입력 옵션부터 볼게요.

  • max_nesting(Integer): 허용하는 최대 중첩 깊이. 기본 100. false를 주면 깊이 검사를 끄고요. 너무 깊으면 JSON::NestingError가 나요.
# Raises JSON::NestingError (nesting of 2 is too deep):
JSON.parse(source, {max_nesting: 1})
  • allow_duplicate_key(boolean): 객체의 중복 키를 무시할지 에러로 처리할지. 지정하지 않으면 마지막 값이 쓰이고 폐기 경고(deprecation warning)가 나요. true면 마지막 값이 쓰이고, falseJSON::ParserError가 나요.
JSON.parse('{"a": 1, "a":2}') => {"a" => 2}
# warning: detected duplicate keys in JSON object.
# This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`
  • allow_nan(boolean): sourceNaN, Infinity, MinusInfinity를 허용할지. 기본 false. 기본이면 파싱 시 JSON::ParserError가 나요.
source = '[NaN, Infinity, -Infinity]'
ruby = JSON.parse(source, {allow_nan: true})
ruby # => [NaN, Infinity, -Infinity]
  • allow_trailing_comma(boolean): 객체·배열의 후행 쉼표를 허용할지. 기본 false. 켜면 JSON.parse('[1,]', allow_trailing_comma: true) # => [1] 돼요.

출력 옵션도 볼게요.

  • freeze(boolean): 반환된 객체를 동결할지. 기본 false.
  • symbolize_names(boolean): 반환된 Hash 키를 Symbol로 만들지. 기본 false(String 사용).
  • object_class(Class): 각 JSON 객체에 쓸 Ruby 클래스. 기본 Hash.
  • array_class(Class): 각 JSON 배열에 쓸 Ruby 클래스. 기본 Array.
  • create_additions: 파싱에 JSON additions를 쓸지. [JSON Additions]를 봐요.

JSON 생성하기

JSON 데이터를 담은 Ruby 문자열을 만들려면 JSON.generate(source, opts)를 써요.

Ruby Array에서 만들면 JSON 배열 문자열이 나오고, Ruby Hash에서 만들면 JSON 객체 문자열이 나와요. 어느 쪽이든 깊이 제한 없이 중첩을 담을 수 있어요.

ruby = [0, 's', :foo]
json = JSON.generate(ruby)
json # => '[0,"s","foo"]'

ruby = {foo: 0, bar: 's', baz: :bat}
json = JSON.generate(ruby)
json # => '{"foo":0,"bar":"s","baz":"bat"}'

배열·해시가 아닌 다른 객체라면 클래스에 따라 달라져요.

JSON.generate(42) # => '42'
JSON.generate(0.42) # => '0.42'
JSON.generate('A string') # => '"A string"'
JSON.generate(true) # => 'true'
JSON.generate(nil) # => 'null'
JSON.generate(:foo) # => '"foo"'
JSON.generate(Complex(0, 0)) # => '"0+0i"'
JSON.generate(Dir.new('.')) # => '"#<Dir>"'

생성 옵션

입력 옵션: allow_nan(기본 false, NaN·Infinity 생성 금지), allow_duplicate_key(중복 키 허용/에러, 기본은 경고), max_nesting(기본 100, 너무 깊으면 JSON::NestingError).

이스케이프 옵션: script_safe(boolean)는 '\u2028', '\u2029', '/'를 이스케이프해서 JSON 객체를 스크립트 태그에 끼워 넣어도 안전하게 만들어요. ascii_only(boolean)는 ASCII 범위 밖의 모든 문자를 이스케이프해요.

출력 옵션: 기본 포맷은 가장 컴팩트한 JSON(한 줄에 공백 없음)이에요. 포맷 옵션으로 공백을 써서 더 여유 있는 형태를 만들 수 있어요. array_nl, object_nl(배열·객체 뒤 삽입할 문자열, 기본 ''), indent(들여쓰기 문자열, 기본 ''), space(객체 쌍의 콜론 뒤 문자열), space_before(콜론 앞 문자열)가 있어요.

obj = {foo: [:bar, :baz], bat: {bam: 0, bad: 1}}
json = JSON.generate(obj)
puts 'Compact:', json
opts = {
  array_nl: "\n",
  object_nl: "\n",
  indent: '  ',
  space_before: ' ',
  space: ' '
}
puts 'Open:', JSON.generate(obj, opts)
Compact:
{"foo":["bar","baz"],"bat":{"bam":0,"bad":1}}
Open:
{
  "foo" : [
    "bar",
    "baz"
],
  "bat" : {
    "bam" : 0,
    "bad" : 1
  }
}

JSON.pretty_generate를 쓰면 들여쓰기·공백·줄바꿈이 기본 적용된 형태로 만들 수 있어요.

JSON Additions (추가 기능)

JSON additions는 신뢰할 수 있는 데이터에만 써야 하고 폐기(deprecated) 예정이에요. 비-String 객체를 Ruby → JSON → Ruby로 왕복(round trip)시키면 원래 객체 대신 새 String을 얻게 되는데, additions는 원래 객체를 보존해 줘요.

  • JSON.generate는 JSON 문자열에 더 많은 정보를 저장하고,
  • JSON.parsecreate_additions 옵션과 함께 호출하면 그 정보로 제대로 된 Ruby 객체를 만들어요.

Range를 addition 없이/있이 왕복시키는 예시를 볼게요.

ruby = Range.new(0, 2)
# This passage does not use the addition for Range.
json0 = JSON.generate(ruby)
ruby0 = JSON.parse(json0)
# This passage uses the addition for Range.
require 'json/add/range'
json1 = JSON.generate(ruby)
ruby1 = JSON.parse(json1, create_additions: true)
Generated JSON:
  Without addition:  "0..2" (String)
  With addition:     {"json_class":"Range","a":[0,2,false]} (String)
Parsed JSON:
  Without addition:  "0..2" (String)
  With addition:     0..2 (Range)

내장 additions

JSON 모듈은 특정 클래스들에 대한 additions를 포함하고 있어요. 사용하려면 해당 소스를 require하면 돼요: BigDecimal(json/add/bigdecimal), Complex(json/add/complex), Date(json/add/date), DateTime(json/add/date_time), Exception(json/add/exception), OpenStruct(json/add/ostruct), Range(json/add/range), Rational(json/add/rational), Regexp(json/add/regexp), Set(json/add/set), Struct(json/add/struct), Symbol(json/add/symbol), Time(json/add/time).

예를 들어 Rangerequire 'json/add/range'JSON.generate하면 {"json_class":"Range","a":[0,2,false]}가 나오고, create_additions: true로 파싱하면 다시 Range가 돼요.

커스텀 JSON additions

클래스에 to_json과 클래스 메서드 json_create를 정의하면 나만의 addition을 만들 수 있어요.

class Foo
  # Serialize Foo object with its class name and arguments
  def to_json(*args)
    {
      JSON.create_id  => self.class.name,
      'a'             => [ bar, baz ]
    }.to_json(*args)
  end
  # Deserialize JSON string by constructing new Foo object with arguments.
  def self.json_create(object)
    new(*object['a'])
  end
end

상수 (Constants)

  • Fragment — 그대로 포함할 JSON 문서 조각. JSON::Fragment.new("[1, 2, 3]")처럼 만들고, 여러 조각을 파싱하거나 문자열 보간 없이 조립할 수 있어요. 유효성 검사는 하지 않으니 문자열이 유효한 JSON인지는 호출자가 보장해야 해요.
  • Infinity, MinusInfinity, NaN — JSON에서 허용 여부를 다루는 특수 값들.
  • JSON_LOADED, PARSE_L_OPTIONS, PRETTY_GENERATE_OPTIONS, VERSION.

속성 (Attributes)

  • generator [R]JSON이 쓰는 generator 모듈.
  • parser [R]JSON이 쓰는 parser 클래스.
  • state [RW]JSON이 쓰는 generator state 클래스의 설정/반환.

클래스 메서드

JSON[object] → new_array or new_string

object가 String이면 JSON.parse를, 아니면 JSON.generate를 호출해요.

json = '[0, 1, null]'
JSON[json]# => [0, 1, nil]
ruby = [0, 1, nil]
JSON[ruby] # => '[0,1,null]'
create_id()

현재 create 식별자를 돌려줘요. 기본값은 'json_class'예요.

create_id=(new_value)

create 식별자를 설정해요. 클래스의 json_create 훅을 호출할지 결정하는 데 써요. 초기값은 json_class예요.

인스턴스 메서드

dump(obj, io = nil, limit = nil)

obj를 JSON 문자열로 덤프해요. 곧 generate를 호출해서 결과를 돌려줘요. io를 주면 JSON String을 io에 쓰고 io를 돌려줘요. limit을 주면 JSON.generatemax_nesting 옵션으로 전달돼요.

obj = {foo: [0, 1], bar: {baz: 2, bat: 3}, bam: :bad}
json = JSON.dump(obj)
json # => "{\"foo\":[0,1],\"bar\":{\"baz\":2,\"bat\":3},\"bam\":\"bad\"}"
fast_generate(obj, opts) → new_string

JSON.generate와 같은 인자예요. 기본적으로 obj의 순환 참조를 검사하지 않고 생성해요(max_nestingfalse). 폐기 예정이니 JSON.generate를 쓰세요.

generate(obj, opts = nil) → new_string

생성된 JSON 데이터를 담은 String을 돌려줘요. obj는 JSON으로 바꿀 Ruby 객체, opts는 생성 옵션 해시예요. Array면 JSON 배열을, Hash면 JSON 객체를 돌려줘요. formatting 옵션이 String이 아니거나 obj에 순환 참조가 있으면 예외가 나요.

load(source, options = {}) → object
load(source, proc = nil, options = {}) → object

주어진 source를 파싱해 만든 Ruby 객체를 돌려줘요. 주의: 이 메서드는 신뢰할 수 있는 사용자 입력(자신의 DB 서버나 통제 아래 있는 클라이언트 등) 데이터를 직렬화하기 위한 거예요. 신뢰할 수 없는 사용자가 JSON 소스를 넘기게 하면 위험할 수 있어요. 어쩔 수 없이 써야 한다면 JSON.unsafe_load를 써서 명확히 하세요.

JSON 2.8.0부터 loadcreate_additions가 명시적으로 켜지지 않은 상태에서 비-고유(non native) 타입을 역직렬화하면 폐기 경고를 내보내요. JSON 3.0에서는 create_additions가 기본적으로 꺼질 예정이에요.

source는 String이거나 String으로 변환 가능해야 해요(to_str, to_io, read에 응답). proc을 주면 각 결과에 재귀적으로 호출돼요(깊이 우선). opts는 파싱 옵션 해시예요.

source = <<~JSON
  {
    "name": "Dave",
    "age" :40,
    "hats": [
      "Cattleman's",
      "Panama",
      "Tophat"
    ]
  }
JSON
ruby = JSON.load(source)
ruby # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
load_file(path, opts={}) → object

parse(File.read(path), opts)를 호출해요.

load_file!(path, opts = {})

JSON.parse!(File.read(path, opts))를 호출해요.

parse(source, opts) → object

주어진 source를 파싱해 만든 Ruby 객체를 돌려줘요. JSON 배열이면 Ruby Array, JSON 객체면 Ruby Hash를 돌려주는 건 앞에서 봤죠. source가 유효한 JSON이 아니면 예외가 나요.

parse!(source, opts) → object

JSON.parse와의 차이: max_nesting이 주어지지 않으면 기본 false(중첩 깊이 검사 끄기), allow_nan이 주어지지 않으면 기본 true.

pretty_generate(obj, opts = nil) → new_string

기본 옵션으로 예쁘게 포맷된 JSON을 만들어요. 기본 옵션은 indent: ' '(두 칸), space: ' '(한 칸), array_nl: "\n", object_nl: "\n"이에요.

obj = {foo: [:bar, :baz], bat: {bam: 0, bad: 1}}
json = JSON.pretty_generate(obj)
puts json
{
  "foo": [
    "bar",
    "baz"
  ],
  "bat": {
    "bam": 0,
    "bad": 1
  }
}
unsafe_load(source, options = {}) → object
unsafe_load(source, proc = nil, options = {}) → object

JSON.load와 동작이 같지만, 의도적으로 "안전하지 않을 수 있음"을 이름으로 드러내요. 신뢰할 수 없는 사용자 입력을 여기에 넣으면 위험할 수 있어요. source·proc·opts 인자는 load와 같아요.