[Scala] Circe를 통한 JSON 문자열(JSON String)에서 Property 삭제 시 주의해야 할 점

Wan Geun Lee / September 16, 2020

문제

Scala
0
1
2
3
4
5
6
7
8
9
val jsonString = """
  {
    "user": {
      "name": "WG Lee",
      "email": "test@example.com",
      "phone_number": "000-0000-0000",
      "address": "Jungjadong, Korea"
    }
  }
"""
  • Scala, Circe 환경에서 JSON 문자열(JSON String)에서 특정 프로퍼티 자체를 삭제하려면 어떻게 할까?
  • 예를 들어 위의 jsonString에서 email과 phone_number, address 라는 프로퍼티들을 삭제하려면 어떻게 할까?

 

해결

  • Circe로 JSON으로 로딩한 뒤 지우려고 하는 프로퍼티에 접근했을 때 delete 함수를 호출해주면 된다.
  • 백문이 불여일견. 코드를 살펴보자.
Scala
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import io.circe._
import io.circe.parser._

val jsonString = """
  {
    "user": {
      "name": "WG Lee",
      "email": "test@example.com",
      "phone_number": "000-0000-0000",
      "address": "Seoul, Korea"
    }
  }
"""

def deleteJsonProperty(jsonStr: String): String = {
  val json = parse(jsonString) match {
    case Right(j) => j
    case _ => Json.Null
  }

  val deleteEmail: ACursor = json.hcursor.downField("user").downField("email").delete
  val deletePhoneNumber = deleteEmail.top.getOrElse(Json.Null).hcursor.downField("user").downField("phone_number").delete
  val deleteAddress = deletePhoneNumber.top.getOrElse(Json.Null).hcursor.downField("user").downField("address").delete
  deleteAddress.top.getOrElse(Json.Null).noSpaces
}

deleteJsonProperty(jsonString)
  • 좀 더 부연 설명을 하자면 circe json에서 hcursor을 이용해 downField로 지우려고 하는 프로퍼티에 접근한 뒤 delete 메소드를 호출하면 해당 프로퍼티가 삭제된 json에 대한 ACursor가 반환된다.
  • 그러면 프로퍼티가 삭제된 ACursor에서 또 프로퍼티를 삭제하기 위해 커서의 위치를 처음(top)으로 되돌린 뒤 또 다시 downField와 delete를 이용해 프로퍼티를 삭제한 ACursor를 생성한다.
  • 즉 원본은 훼손되지 않고 특정 프로퍼티만 삭제된 ACursor를 계속 새로 생성한다.
  • 그런데 여기엔 심각한 버그가 있다. 뭘까?

 

근데 해결책에 버그가 있다는데?

지우려는 프로퍼티가 모두 있을 때 위 코드는 문제가 없다. 문제는 지우려고 하는 프로퍼티가 하나라도 없을 때 위에 구현한 deleteJsonProperty 메소드는 null을 반환한다.

Scala
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import io.circe._
import io.circe.parser._

val jsonString = """
  {
    "user": {
      "name": "WG Lee",
      "phone_number": "000-0000-0000",
      "address": "Seoul, Korea"
    }
  }
"""

def deleteJsonProperty(jsonStr: String): String = {
  val json = parse(jsonString) match {
    case Right(j) => j
    case _ => Json.Null
  }

  val deleteEmail: ACursor = json.hcursor.downField("user").downField("email").delete
  val deletePhoneNumber = deleteEmail.top.getOrElse(Json.Null).hcursor.downField("user").downField("phone_number").delete
  val deleteAddress = deletePhoneNumber.top.getOrElse(Json.Null).hcursor.downField("user").downField("address").delete
  deleteAddress.top.getOrElse(Json.Null).noSpaces
}

deleteJsonProperty(jsonString)

jsonString을 보면 user.email이라는 JSON 프로퍼티는 없다. 하지만 deleteJsonProperty 함수의 내부를 보면 user.email을 삭제하려는 코드가 있다. 그럼 어떻게 될까?

일단 프로퍼티가 없기 때문에 삭제를 할 수가 없다. 또한 phone_number 프로퍼티를 삭제하기 위해 deleteEmail.top 코드를 수행하게 되면 None을 반환한다. 프로퍼티를 삭제하지 못했기 때문에 아무것도 가리키지 않는 ACursor가 반환 되었는데 여기에 top 메소드를 호출하면 아무것도 없기 때문에 None을 반환한다. 여기서 또 친절하게 None일 때 Json.Null을 반환하여 프로퍼티 삭제 동작의 일관성을 유지하고 있다.

그런데 문제는 여기서 부터 시작된다. 그 다음인 user.phone_number라는 JSON 프로퍼티는 있지만 프로퍼티 삭제를 할 수가 없다. 왜냐하면 deleteEmail.top.getOrElse(Json.Null)에서 Json.Null을 받았기 때문에 삭제할 프로퍼티가 없어서 JSON 프로퍼티 삭제가 진행되질 않는다. (여기서도 친절하게 프로퍼티가 없어도 downField는 예외를 발생시키지 않는다)

결국 위 deleteJsonProperty 함수를 호출하면 삭제하려는 프로퍼티 user.email, user.phone_number, user.address 중에 한 개라도 없을 때 "null" 이라는 문자열을 반환한다.

 

버그는 어떻게 해결할까?

일단 생각 나는대로 해결하자면 delete 메소드를 통한 프로퍼티 삭제 후 다음 프로퍼티를 삭제하기 위해 top 메소드를 호출 했을 때 없으면(None이면) delete 메소드를 통한 프로퍼티 삭제하기 이전 커서로 되돌리면 될거 같다. 코드로 옮기면 다음과 같다.

Scala
0
1
2
3
4
5
val rootCursor = json.hcursor
val deleteEmail = rootCursor.downField("user").downField("email").delete.top.map(_.hcursor).getOrElse(rootCursor)
val deletePhoneNumber = deleteEmail.downField("user").downField("phone_number").delete.top.map(_.hcursor).getOrElse(deleteEmail)
val deleteAddress = deletePhoneNumber.downField("user").downField("address").delete.top.map(_.hcursor).getOrElse(deletePhoneNumber)

deletePhoneNumber.focus.map(_.noSpaces).getOrElse("...")

이렇게 하면 없는 프로퍼티를 삭제하려고 하면 top으로 다시 돌아갔을 때 None을 반환하는건 마찬가지지만 None에 대한 getOrElse는 이전 커서를 반환했기 때문에 빈 프로퍼티를 반환하지 않게 된다.

 

더 깔끔한 해결 방법이 있을거 같긴 하지만 일단 포스팅은 여기서 마친다.