二十、XML 和 JSON

在第 19 章中,我们学习了如何在我们的 Android 项目中包含外部库。Kotlin 的标准库中没有专门的 XML 和 JSON 处理类,所以为了完成 XML 和 JSON 相关的任务,我们使用适当的外部库,并以扩展函数的形式添加一些方便的函数。

注意

XML 和 JSON 都是结构化数据的格式规范。如果您的 Android 应用与外部世界通信以接收或发送标准化格式的数据,您将会经常使用它们。

本章假设您有一个示例应用,可以用来测试所提供的代码片段。使用你喜欢的任何应用或我们在本书中开发的应用之一。例如,您可以添加一些示例代码,为 activity 的onCreate()函数内部的测试提供Log输出,或者您可以使用一个使用 Android 测试方法的测试类。选择最适合您需求的方法。

XML 处理

XML 文件最简单的形式如下:

<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
  <TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
  <ProbeId>1A6G</ProbeId>
  <ProbeValue ScaleUnit="cm">37.4</ProbeValue>
  <Meta>
    <Generator>045</Generator>
    <Priority>-3</Priority>
    <Actor>P. Rosengaard</Actor>
  </Meta>
</ProbeMsg>

注意

XML 允许更复杂的结构,如模式验证和名称空间。在本章中,我们只描述 XML 标签、属性和文本内容。您可以自由扩展本章中介绍的示例和实用程序函数,以包含这些扩展功能。

对于 XML 处理,使用以下范例之一或组合。

  • DOM 模型:完整的树处理:在文档对象模型(DOM)中,XML 数据被视为一个整体,由内存中的树状结构表示。

  • SAX:基于事件的处理:在这里,XML 文件被解析,每个元素或属性都会触发一个适当的事件。事件由回调函数接收,回调函数必须向 SAX 处理器注册。这种“告诉我你在做什么”的处理方式通常被称为推送解析

  • StAX:基于流的处理:在这里,您执行诸如“给我下一个 XML 元素”之类的操作。与 SAX 不同,在 SAX 中我们有一个推送解析,对于 StAX,我们告诉解析器它必须做什么:“我告诉你你做什么。”这因此被称为拉解析

在 Android 上,你通常处理小到中等大小的 XML 文件。因此,在本章中我们使用 DOM。对于读取,我们首先解析完整的 XML 文件,并将数据存储在内存中的 DOM 树中。在这里,像删除、更改或添加元素这样的操作很容易完成,并且发生在内存中;因此它们非常快。为了编写,我们从内存中取出完整的 DOM 树,并从中生成一个 XML 字符流,也许将结果写回到一个文件中。

对于 XML 处理,我们添加了 Java 参考实现 Xerces 作为外部库。在 Android Studio 中,打开模块的build.gradle文件,在dependencies部分添加:

implementation 'xerces:xercesImpl:2.12.0'

注意

Xerces 还实现了 SAX 和 StAX APIs,尽管我们将只使用它的 DOM 实现。

读取 XML 数据

借助 Xerces 实现,我们可以使用的 DOM 实现已经包含了读取 XML 元素所需的一切。然而,我们将添加几个扩展函数,大大提高 DOM API 的可用性。为此,创建一个包com.example.domext,或者您也可以使用任何其他合适的包名。在这个包中添加一个 Kotlin 文件dom.kt,其内容如下:

package com.example.domext

import org.apache.xerces.parsers.DOMParser
import org.w3c.dom.Document
import org.w3c.dom.Node
import org.xml.sax.InputSource
import java.io.StringReader
import java.io.StringWriter
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult

fun parseXmlToDOM(s:String) : Document {
  val parser: DOMParser = DOMParser()
  return parser.let {
      it.parse(InputSource(StringReader(s)))
      it.document
  }
}

fun Node.fetchChildren(withText:Boolean = false) =
  (0..(this.childNodes.length - 1)).
  map { this.childNodes.item(it) }.
  filter { withText || it.nodeType != Node.TEXT_NODE }

fun Node.childCount() = fetchChildren().count()

fun Node.forEach(withText:Boolean = false,
                 f:(Node) -> Unit) {
  fetchChildren(withText).forEach { f(it) }
}

operator fun Node.get(i:Int) = fetchChildren()[i]
operator fun Node.invoke(s:String): Node =
  if(s.startsWith("@")) {
      this.attributes.getNamedItem(s.substring(1))
  }else{
      this.childNodes.let { nl ->
          val iter = object : Iterator<Node> {
              var i: Int = 0
              override fun next() = nl.item(i++)
              override fun hasNext() = i < nl.length
          }
          iter.asSequence().find { it.nodeName == s }!!
      }
  }

operator fun Node.invoke(vararg s:String): Node =
    s.fold(this, { acc, s1 -> acc(s1)  })

fun Node.text() = this.firstChild.nodeValue
fun Node.name() = this.nodeName
fun Node.type() = this.nodeType

这些都是org.w3c.dom.Node的包级函数和扩展函数,具有以下特点:

  • 在 DOM API 中,树中的每个元素(例如,从本节开始的 XML 数据中的ProbeValue)都由一个Node实例表示。

  • 我们添加了一个parseXmlToDOM(s:String)包级函数,将 XML 字符串转换为Document

  • 我们给Node添加了一个fetchChildren()函数,它返回一个节点的所有非文本子节点,这些子节点忽略了文本元素。如果添加with- Text=true作为参数,元素的文本节点会包含在子列表中,即使它们只包含空格和换行符。例如,在本节开头的 XML 数据中,节点Meta有三个子节点:GeneratorPriorityActor。使用withText=true,它们之间的空格和换行符也将被返回。

  • 我们给Node添加了一个childCount()函数,它计算一个节点的子元素的数量,不考虑文本元素。官方的 DOM API 没有为此提供功能。

  • 我们给Node添加了一个forEach()函数,允许我们以 Kotlin 的方式遍历一个节点的子节点。最初的 DOM API 没有提供这样的迭代器,因为它只有函数和属性hasChild- Nodes()childNodes.lengthchildNodes.item(index:Int)来遍历子元素。如果添加withText=true作为参数,元素的文本节点将包含在子列表中,即使它们只包含空格和换行符。

  • 我们为Node添加了一个get(i:Int)函数,以从元素中获取某个子元素,而不考虑文本节点。

  • 我们重载了Nodeinvoke运算符,属于括号()。带有String参数的第一个变量通过名称导航到一个孩子:node("cn") = node →名为“cn”的孩子如果参数以一个@开始,属性被寻址:node("@an") = node →属性名为“an”在后一种情况下,您仍然需要调用text()来获得字符串形式的属性值。

  • 重载的invoke操作符的第二个变体允许我们指定几个字符串,从一个子元素导航到另一个子元素,等等。

  • 我们向Node添加函数:首先,text()获取元素的文本内容,然后name()给出节点名,然后type()计算节点类型(可能的值参见Node类的常量属性)。

警告

为了简单起见,本节中显示的 DOM 处理代码片段没有以合理的方式处理异常。在将代码用于生产项目之前,必须添加适当的错误处理。

这个片段提供了如何使用 API 和扩展的例子。

import ...
import com.example.domext.*

...
val xml = """<?xml version="1.0" encoding="UTF-8"?>
  <ProbeMsg>
    <TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
    <ProbeId>1A6G</ProbeId>
    <ProbeValue ScaleUnit="cm">37.4</ProbeValue>
  <Meta>
    <Generator>045</Generator>
    <Priority>-3</Priority>
    <Actor>P. Rosengaard</Actor>
  </Meta>
</ProbeMsg>"""

try {
    // Parse the complete XML document
    val dom = parseXmlToDOM(xml)

    // Access an element
    val ts = dom("ProbeMsg")("TimeStamp").text()
    Log.d("LOG", ts) // 2001-11-30T09:08:07Z

    // Access an attribute
    val uni = dom("ProbeMsg")("ProbeValue")("@ScaleUnit")
    Log.d("LOG", uni.text()) // cm

    // Simplified XML tree navigation
    val uni2 = dom("ProbeMsg","ProbeValue","@ScaleUnit")
    Log.d("LOG", uni2.text()) // cm

    // Iterate through an element's children
    dom("ProbeMsg")("Meta").forEach { n ->
        Log.d("LOG", n.name() + ": " + n.text())
        //    Generator: 045
        //    Priority: -3
        //    Actor: P. Rosengaard
    }
}catch(e:Exception) {
    Log.e("LOG", "Cannot parse XML", e)
}
...

改变 XML 数据

一旦我们在内存中有了 XML 树的 DOM 表示,我们就可以添加元素了。虽然我们可以使用 DOM API 已经提供的函数,但是 Kotlin 允许我们提高表达能力。为此,将以下代码添加到我们的扩展文件dom.kt(我不添加新的导入;按 Alt+Enter 让 Android Studio 帮你添加必要的导入):

fun prettyFormatXml(document:Document): String {
  val format = OutputFormat(document).apply { lineWidth = 65
      indenting = true
      indent = 2
  }
  val out = StringWriter()
  val serializer = XMLSerializer(out, format)
  serializer.serialize(document)
  return out.toString()
}

fun prettyFormatXml(unformattedXml: String) =
      prettyFormatXml(parseXmlToDOM(unformattedXml))

fun Node.toXmlString():String {
  val transformerFact = TransformerFactory.newInstance()
  val transformer = transformerFact.newTransformer()
  transformer.setOutputProperty(OutputKeys.INDENT, "yes")
  val source = DOMSource(this)
  val writer = StringWriter()
  val result = StreamResult(writer)
  transformer.transform(source, result)
  return writer.toString()
}

operator fun Node.plusAssign(child:Node) {
  this.appendChild(child)
}

fun Node.addText(s:String): Node {
  val doc = ownerDocument
  val txt = doc.createTextNode(s)
  appendChild(txt)
  return this
}

fun Node.removeText() {
  if(hasChildNodes() && firstChild.nodeType == Node.TEXT_NODE)
     removeChild(firstChild)
}

fun Node.updateText(s:String) : Node { removeText()
  return addText(s)
}

fun Node.addAttribute(name:String, value:String): Node {
  (this as Element).setAttribute(name, value)
  return this
}

fun Node.removeAttribute(name:String) {
    this.attributes.removeNamedItem(name)
}

这是我们在这种情况下的描述

  • 功能prettyFormatXml( document: Document )prettyFormatXml( unformattedXml: String )是主要用于诊断目的的实用功能。给定一个Document或者一个无格式的 XML 字符串,它们创建一个漂亮的字符串。

  • 扩展函数Node.toXmlString()从当前节点开始创建 XML 子树的字符串表示。如果对Document这样做,整个 DOM 结构都将被转换。

  • 我们重载NodeplusAssign操作符(对应于+=)来添加一个子节点。

  • 我们为Node添加了一个addText()扩展,用于向节点添加文本内容。

  • 我们为Node添加了一个removeText()扩展,用于从节点中删除文本内容。

  • 我们为Node添加了一个updateText()扩展,用于改变节点的文本内容。

  • 我们为Node添加了一个addAttribute()扩展,用于向节点添加属性。

  • 我们为Node添加了一个removeAttribute()扩展,用于从节点中删除属性。

  • 我们为Node添加了一个updateAttribute()扩展,用于改变节点的属性。

例如,这些函数的用例包括以下代码片段。首先,我们向给定的节点添加一个元素加属性:

val xml = """<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
  <TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
  <ProbeId>1A6G</ProbeId>
  <ProbeValue ScaleUnit="cm">37.4</ProbeValue>
  <Meta>
    <Generator>045</Generator>
    <Priority>-3</Priority>
    <Actor>P. Rosengaard</Actor>
  </Meta>
</ProbeMsg>"""

    try {
        val dom = parseXmlToDOM(xml)

        val msg = dom("ProbeMsg")
        val meta = msg("Meta")

        // Add a new element to "meta".
        meta += dom.createElement("NewMeta").
            addText("NewValue").
            addAttribute("SomeAttr", "AttrVal")

        Log.d("LOG", "\n\n" + prettyFormatXml(dom))

    }catch(e:Exception) { Log.e("LOG", "XML Error", e)
}

为此,我们还使用了来自Document类的createElement()函数。最后,这段代码将修改后的 XML 写入日志控制台。

以下代码示例解释了如何更改和移除属性和元素:

val xml = """<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
  <TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
  <ProbeId>1A6G</ProbeId>
  <ProbeValue ScaleUnit="cm">37.4</ProbeValue>
  <Meta>
    <Generator>045</Generator>
    <Priority>-3</Priority>
    <Actor>P. Rosengaard</Actor>
  </Meta>
</ProbeMsg>"""

    try {
        val dom = parseXmlToDOM(xml)

        val msg = dom("ProbeMsg")
        val ts = msg("TimeStamp")
        val probeValue = msg("ProbeValue")

        // Update an attribute and the text contents of
        // an element.
        probeValue.updateAttribute("ScaleUnit", "dm")
        ts.updateText("1970-01-01T00:00:00Z")
        Log.d("LOG", "\n\n" + prettyFormatXml(dom))

        // Remove an attribute
        probeValue.removeAttribute("ScaleUnit")
        Log.d("LOG", "\n\n" + prettyFormatXml(dom))

        // Removing a node means removing it from
        // its parent node.
        msg.removeChild(probeValue)
        Log.d("LOG", "\n\n" + prettyFormatXml(dom))

}catch(e:Exception) {
    Log.e("LOG", "XML Error", e)
}

创建新的 DOM

如果您需要从头开始编写 XML 文档的 DOM 表示,首先创建一个Document实例。这个没有公共构造函数;相反,你应该写:

val doc = DocumentBuilderFactory.
      newInstance().newDocumentBuilder().newDocument()

从这里,您可以像前面描述的那样添加元素。注意,要查看我们的prettyFormatXml()实用函数的任何输出,您必须向doc添加至少一个子元素。

练习 1

dom.kt文件添加一个createXmlDocument()函数,以简化文档创建。

JSON 处理

JavaScript 对象表示法(JSON)是 XML 的小姐妹。与使用 XML 格式的数据相比,用 JSON 格式编写的数据需要更少的空间。此外,JSON 数据几乎自然地映射到浏览器环境中的 JavaScript 对象,因此 JSON 在最近几年获得了相当大的关注。

Kotlin 的标准库不知道如何处理 JSON 数据,所以,类似于 XML 处理,我们添加一个合适的外部库。从几种可能性来看,我们使用广泛采用的杰克逊图书馆。要将它添加到 Android 项目中,在模块的build.gradle文件中的dependencies部分添加

implementation
    'com.fasterxml.jackson.core:jackson-core:2.9.8'
implementation
    'com.fasterxml.jackson.core:jackson-databind:2.9.8'

(在两行上,删除换行符)。

JSON 处理有几种范例。最常用的是带有特定于 JSON 的对象的树状结构,以及带有各种半自动转换机制的 Kotlin 和 JSON 对象之间的映射。我们将映射方法留给您进一步的研究;它包含几个非常复杂的特性,主要是 JSON 集合映射。杰克逊的主页给了你更多的信息。相反,我们描述处理 JSON 数据的内存树表示的机制。

对于本节的其余部分,我们使用以下 JSON 数据来解释示例中使用的函数:

val json = """{
   "id":27,
   "name":"Roger Rabbit",
   "permanent":true,
   "address":{
       "street":"El Camino Real",
       "city":"New York",
       "zipcode":95014
   },
   "phoneNumbers":[9945678, 123456781],
   "role":"President"
}"""

JSON 助手函数

用于 JSON 处理的 Jackson 库包含了编写、更新和删除 JSON 元素所需的全部内容。这个库非常广泛,包含了大量的类和函数。为了简化开发并包含 Kotlin 好东西,我们使用了一些包级函数和扩展函数来提高 JSON 代码的可读性。这些最好放在某个包com.whatever.ext中的 Kotlin 文件json.kt中。

我们从导入开始,添加一个invoke操作符,这样我们可以很容易地从一个节点获取一个子节点,并添加一个remove和一个forEach函数,用于删除一个节点并遍历节点的子节点:

import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.*
import java.io.ByteArrayOutputStream
import java.math.BigInteger

operator fun JsonNode.invoke(s:String) = this.get(s)
operator fun JsonNode.invoke(vararg s:String) =
    s.fold(this, { acc, s -> acc(s) })
fun JsonNode.remove(name:String) {
    val on = (this as? ObjectNode)?:
        throw Exception("This is not an object node")
    on.remove(name) }
fun JsonNode.forEach(iter: (JsonNode) -> Unit ) {
    when(this) {
        is ArrayNode -> this.forEach(iter)
        is ObjectNode -> this.forEach(iter)
        else -> throw Exception("Cannot iterate over " +
              this::class)
    }
}

接下来,我们为asText()添加简单的别名text(),以简化文本提取:

fun JsonNode.text() = this.asText()

另一个迭代器遍历对象节点的子节点。这一次,我们还会考虑孩子们的名字:

fun JsonNode.forEach(iter: (String, JsonNode) -> Unit ) {
    if(this !is ObjectNode)
        throw Exception(
        "Cannot iterate (key,val) over " + this::class)
    this.fields().forEach{
        (name, value) -> iter(name, value) }
}

为了编写一个对象节点的子节点,我们定义了一个put()函数,因此我们可以编写node.put( "childName", 42 ):

// Works only if the node is an ObjectNode!
fun JsonNode.put(name:String, value:Any?) : JsonNode {
    if(this !is ObjectNode)
        throw Exception("Cannot put() on none-object node")
    when(value) {
        null -> this.putNull(name)
        is Int -> this.put(name, value)
        is Long -> this.put(name, value)
        is Short -> this.put(name, value)
        is Float -> this.put(name, value)
        is Double -> this.put(name, value)
        is Boolean -> this.put(name, value)
        is String -> this.put(name, value)
        is JsonNode -> this.put(name, value)
        else -> throw Exception(
            "Illegal value type: ${value::class}")
    }
    return this
}

为了向数组对象追加一个值,我们定义了一个add()函数,它适用于各种类型:

// Add a value to an array, works only if this is an
// ArrayNode
fun JsonNode.add(value:Any?) : JsonNode {
    if(this !is ArrayNode)
        throw Exception("Cannot add() on none-array node")
    when(value) {
        null -> this.addNull()
        is Int -> this.add(value)
        is Long -> this.add(value)
        is Float -> this.add(value)
        is Double -> this.add(value)
        is Boolean -> this.add(value)
        is String -> this.add(value)
        is JsonNode -> this.add(value)
        else -> throw Exception(
            "Illegal value type: ${value::class}")
    }
    return this
}

对于 JSON 对象创建,我们定义了各种createSomething()样式的函数,并且我们还添加了几个类似 Kotlin 的构建函数:

// Node creators
fun createJsonTextNode(text:String) = TextNode.valueOf(text)
fun createJsonIntNode(i:Int) = IntNode.valueOf(i)
fun createJsonLongNode(l:Long) = LongNode.valueOf(l)
fun createJsonShortNode(s:Short) = ShortNode.valueOf(s)
fun createJsonFloatNode(f:Float) = FloatNode.valueOf(f)
fun createJsonDoubleNode(d:Double) = DoubleNode.valueOf(d)
fun createJsonBooleanNode(b:Boolean) = BooleanNode.valueOf(b)
fun createJsonBigIntegerNode(b: BigInteger) = BigIntegerNode.valueOf(b)
fun createJsonNullNode() = NullNode.instance

fun jsonObjectNodeOf(
      children: Map<String,JsonNode> = HashMap()) :
      ObjectNode {
    return ObjectNode(JsonNodeFactory.instance, children)
}

fun jsonObjectNodeOf(
      vararg children: Pair<String,Any?>) :
      ObjectNode {
    return children.fold(
          ObjectNode(JsonNodeFactory.instance), { acc, v ->
        acc.put(v.first, v.second)
        acc
    })
}
fun jsonArrayNodeOf(elements: Array<JsonNode> =
      emptyArray()) : ArrayNode {
    return ArrayNode(JsonNodeFactory.instance,
                     elements.asList())
}
fun jsonArrayNodeOf(elements: List<JsonNode> =
      emptyList()) : ArrayNode {
    return ArrayNode(JsonNodeFactory.instance,
                     elements)
}
fun jsonEmptyArrayNode() : ArrayNode {
    return ArrayNode(JsonNodeFactory.instance)
}
fun jsonArrayNodeOf(vararg elements: Any?) : ArrayNode {
    return elements.fold(
          ArrayNode(JsonNodeFactory.instance), { acc, v ->
        acc.add(v)
        acc
    })
}

扩展函数toPrettyString()toJsonString()可以用来生成任何 JSON 节点的字符串表示:

// JSON output as pretty string
fun JsonNode.toPrettyString(
        prettyPrinter:PrettyPrinter? =
        DefaultPrettyPrinter()) : String {
    var res:String? = null
    ByteArrayOutputStream().use { os ->
        val gen = JsonFactory().createGenerator(os).apply {
            if(prettyPrinter != null) this.prettyPrinter = prettyPrinter
        }
        val mapper = ObjectMapper()
        mapper.writeTree(gen, this)
        res = String(os.toByteArray())
    }
    return res!!
}

// JSON output as simple string
fun JsonNode.toJsonString() : String =
      toPrettyString(prettyPrinter = null)

所有这些扩展函数的主要思想是通过向基本节点类JsonNode添加 JSON 对象相关和 JSON 数组相关的函数来提高简洁性,并在运行时执行类强制转换。虽然它使 JSON 代码更小,更有表现力,但运行时出现异常的风险也增加了。

读取和写入 JSON 数据

要读入 JSON 数据,您只需编写:

val json = ... // see section beginning
val mapper = ObjectMapper()
val root = mapper.readTree(json)

从这里我们可以研究 JSON 元素,遍历并获取 JSON 对象成员,并提取 JSON 数组元素:

try {
    val json = ... // see section beginning
    val mapper = ObjectMapper()
    val root = mapper.readTree(json)

    // see what we got
    Log.d("LOG", root.toPrettyString())

    // type of the node
    Log.d("LOG", root.nodeType.toString())
    // <- OBJECT
    // is it a container?
    Log.d("LOG", root.isContainerNode.toString())
    // <- true

    root.forEach { k,v ->
        Log.d("LOG",
          "Key:${k} -> val:${v} (${v.nodeType})")
        Log.d("LOG",
          "    <- " + v::class.toString())
    }

    val phones = root("phoneNumbers")
    phones.forEach { ph ->
        Log.d("LOG", "Phone: " + ph.text())
    }
    Log.d("LOG", "Phone[0]: " + phones[0].text())

    val street = root("address")("street").text()
    Log.d("LOG", "Street: " + street)
    Log.d("LOG", "Zip: " + root(“address”, “zipcode”).asInt())

}catch(e:Exception) {
    Log.e("LOG", "JSON error", e)
}

以下代码片段展示了如何通过添加、更改或删除节点或 JSON 对象成员来改变 JSON 树。

// add it to the "try" statements from the
// last listing

// remove an entry
root("address").remove("zipcode")
Log.d("LOG", root.toPrettyString())

// update an entry
root("address").put("street", "Fake Street 42")
Log.d("LOG", root.toPrettyString())

root("address").put("country", createJsonTextNode("Argentina"))
Log.d("LOG", root.toPrettyString())

// create a new object node
root.put("obj", jsonObjectNodeOf(
         "abc1" to 23,
         "abc2" to "Hallo",
         "someNull" to null
))
Log.d("LOG", root.toPrettyString())

// create a new array node
root.put("arr", jsonArrayNodeOf(
         23,
         null,
         "Hallo"
))
Log.d("LOG", root.toPrettyString())

// write without spaces or line breaks
Log.d("LOG", root.toJsonString())

创建新的 JSON 树

要在内存中创建新的 JSON 树,可以使用:

val root = jsonObjectNodeOf()

从那里,您可以像前面描述的那样添加 JSON 元素。

练习 2

创建一个 JSON 文档,对应于:

{
  "firstName": "Arthur",
  "lastName": "Doyle",
  "dateOfBirth": "03/04/1997",
  "address": {
    "streetAddress": "21 3rd Street",
    "city": "New York",
    "state": "NY",
    "postalCode": "10021-1234"
  },
  "phoneNumbers": [
    {
      "type": "home",
      "number": "212 555-1234"
    },
    {
      "type": "mobile",
      "number": "123 456-7890"
    }
  ],
  "children": [],
  "spouse": null
}