[swift] JSON stringify

개요top

JSON데이터를 다루는데 있어서 유명한 swiftyJSON을 쓰거나 NSJSONSerialization를 얇게 래핑해서 쓰는게 보통입니다.
저도 JSON문자열을 파싱할 때는 간단히 NSJSONSerialization를 사용합니다. 하지만 데이터구조를 만들어서 사용하다가 이를 JSON문자열로 만들 때는 딱히 맘에 드는 녀석이 없었습니다.
다들 나름대로의 제약을 갖고 있거나 전역공간에서 연산자나 함수 등을 재 정의하는 것도 충돌을 일으키는 등 문제가 많이 있었습니다.
기본적으로 swift의 Dictionary구조로 기술된 데이터를 JSON문자열로 잘 만들어주면 좋겠다는 소박한 생각을 갖고 간단한 stringify함수를 작성하게 되었습니다.

기본이 되는 형과 클래스의 정의top

우선 반복되는 데이터형을 먼저 정의합니다. 원본자료가 될 Dictionary를 최대한 넓게 정의한다면 [String:Any?] 겠죠. 또한 배열도 마찬가지로 [Any?]가 가장 커버리지가 넓을 것입니다.
이에 대해 간단히 사전 정의합니다.

typealias DIC = [String:Any?]
typealias ARR = [Any?]

또한 JSON문자열에는 특수한 기호가 들어가는데 {, }, [, ] 네가지 괄호기호와 컴마입니다. 이를 구분자로서 인식할 타입이 필요합니다. 간단히 Deter라는 클래스를 정의하죠.

class Deter{
  let val:String
  init(_ v:String){
    val = v
  }
}

이 클래스를 이용해 간단히 구분자를 사전에 정의해 둘 수 있습니다.

static private let sep = Deter(",")
static private let objStart = Deter("{"), objEnd = Deter("}")
static private let arrStart = Deter("["), arrEnd = Deter("]")

헌데 JSON은 기본적으로 키와 값으로 되어있으므로 이게 키인지 값인지 구분할 수 있어야합니다. 키를 위한 형을 정의하여 해결합니다.

class Key{
  let val:String
  init(_ v:String){
    val = v
  }
}

Key클래스든 Deter클래스든 특별히 할 일은 없습니다. 단지 일반적인 값과 구분되는 토큰으로 스택에 들어가길 원하는 거죠.

순환스택을 통한 문자열병합top

대상 Dictionary를 순회하면서 문자열에 더해가되 그 안에 다시 순회를 요구하는 배열이나 Dictionary가 등장하면 스택에 해당 요소를 역순으로 추가해주면 전체 스택루프가 원래 순서로 복원해가며 문자열을 더해가게 됩니다. 간단히 함수의 정의와 스택을 초기화하죠. 또한 JSON문자열이 될 result도 정의합니다.

static func jsonStringify(v:DIC)->String{
  var stack:ARR = [v], result = ""

ARR은 [Any?]이므로 nil값도 받을 수 있습니다. nil요소는 JSON문자열에서는 null로 처리될 것입니다.

이제 스택을 돌면서 그안에 요소를 꺼내가며 문자열에 더해주거나 스택이 추가적인 요소를 더해가는 식으로 작동합니다.

while !stack.isEmpty{ //스택루프

  //마지막요소를 꺼내 처리한다
  if let cur = stack.popLast(){ 
      
     //cur의 여러 형에 대해 처리해준다

  }
}

루프 자체는 굉장히 간단한 구조입니다. 최초 스택에는 인자로 받은 Dictionary가 들어가 있습니다.

각 형별 처리기제작top

위 소스에서 바디에 들어갈 형별 처리기를 작성해보죠. 다음과 같은 전략을 사용합니다.

  1. Deter타입은 val에 있는 문자열을 꺼내 더해주면 된다.
  2. Key 타입은 “Key.val:” 형태가 되도록 만들어 더해준다.
  3. ARR([Any?]) 라면 ]와 배열요소 역순, [를 차례로 스택에 넣어준다. 스택이 역으로 순환하므로 [, 원래순서, ] 형태로 문자열에 더해진다.
  4. DIC이라면 }, 값, 키, 컴마, …, { 형태로 스택에 넣어준다. 스택순환에 의해 {키,컴마,값…} 형태로 문자열에 더해진다.
  5. 만약 문자열이라면 큰따옴표로 감싸줘야한다.
  6. 그 외의 값은 문자열로만 만들어서 더해준다.

위의 전략 그대로를 코드로 옮깁니다.

if cur == nil{
	result += "null"
}else if cur is Deter{
	result += (cur as! Deter).val;
}else if cur is Key{
	result += "\"" + (cur as! Key).val + "\":"
}else if cur is ARR{
	let c = cur as! ARR
	var i = c.count
	stack.append(arrEnd)
	while i > 0 {
		i -= 1
		stack.append(c[i])
		stack.append(sep)
	}
	stack.popLast()
	stack.append(arrStart)
}else if cur is DIC{
	let c = cur as! DIC
	stack.append(objEnd)
	for (k, v) in c{
		stack.append(v)
		stack.append(Key(k))
		stack.append(sep)
	}
	stack.popLast()
	stack.append(objStart)
}else if cur is String{
	result += "\"\(cur!)\""
}else{
	result += "\(cur!)"
}

간단합니다. 전략이 간단하니 코드도 간단하죠. 이상의 루프를 통해 result가 완성됩니다.
이제 테스트를 해볼 수 있겠죠.

jsonStringify([
  "a":3,
  "b":["a0", true, 123] as ARR
] as DIC)

//{"a":3, "b":["a0", true, 123]}

결론top

원래 swift문법에서 간단히 리터럴로 표현할 수 있는 Dictionary로 원하는 데이터를 기술하고 이를 손쉽게 JSON문자열로 환원하는 함수를 제작해봤습니다. 전체 소스는 다음과 같습니다.

extension SomeClass{

  typealias DIC = [String:Any?]
  typealias ARR = [Any?]

  class Key{
    let val:String
    init(_ v:String){
      val = v
    }
  }

  class Deter{
    let val:String
    init(_ v:String){
      val = v
    }
  }

  static private let sep = Deter(",")
  static private let objStart = Deter("{"), objEnd = Deter("}")
  static private let arrStart = Deter("["), arrEnd = Deter("]")

  static func jsonStringify(v:DIC)->String{
    var stack:ARR = [v], result = ""
    while !stack.isEmpty{
      if let cur = stack.popLast(){
        if cur == nil{
          result += "null"
        }else if cur is Deter{
          result += (cur as! Deter).val;
        }else if cur is Key{
          result += "\"" + (cur as! Key).val + "\":"
        }else if cur is ARR{
          let c = cur as! ARR
          var i = c.count
          stack.append(arrEnd)
          while i > 0 {
            i -= 1
            stack.append(c[i])
            stack.append(sep)
          }
          stack.popLast()
          stack.append(arrStart)
        }else if cur is DIC{
          let c = cur as! DIC
          stack.append(objEnd)
          for (k, v) in c{
            stack.append(v)
            stack.append(Key(k))
            stack.append(sep)
          }
          stack.popLast()
          stack.append(objStart)
        }else if cur is String{
          result += "\"\(cur!)\""
        }else{
          result += "\(cur!)"
        }
      }
    }
    return result
  }
}