F# Data


F# Data: JSON と XML の相互変換

このチュートリアルではJSONドキュメント( JSON パーサーの記事 で説明している JsonValue 型で表されるドキュメント)と XMLドキュメント( XElement で表されるドキュメント)を 相互に変換する機能を実装する方法を紹介します。 この機能はF# Dataライブラリから直接使用できるものではありませんが、 JSON(あるいはXML)ドキュメントを再帰的に処理するだけで 非常に簡単に実装できます。

JSONとXML間の変換を自身のコードで使いたい場合には GitHubにあるソース をコピーしてプロジェクトに追加するだけです。 この機能を頻繁に利用することがあり、F# Dataライブラリの機能としてほしい場合には 是非 機能リクエスト を投稿してください。

初期化

ここでは( System.Xml.Linq.dll 内にある)LINQ to XMLのAPIと、 FSharp.Data 名前空間にある JsonValue を使います:

1: 
2: 
3: 
4: 
#r "System.Xml.Linq.dll"
#r "../../../../bin/FSharp.Data.dll"
open System.Xml.Linq
open FSharp.Data

このスクリプト内では簡単に処理できる値を返すような変換機能を実装しますが、 リバーシブルな変換ではないことに注意してください (つまりJSONからXMLに変換した後、JSONに戻したとしても 元と同じ値になるとは限らないということです)。

XMLからJSONへの変換

XMLとJSONはかなり似た形式ではありますが、 細かい部分ではそれなりに違いがあります。 たとえばXMLでは 属性子要素 が区別されます。 さらにすべてのXML要素には名前がありますが、 JSONの配列やレコードは無名です(ただしレコードには 名前のつけられたフィールドがあります)。 たとえば以下のようなXMLがあるとします:

1: 
2: 
3: 
4: 
5: 
<channel version="1.0">
  <title text="Sample input" />
  <item value="First" />
  <item value="Second" />
</channel>

これに対して生成するJSONではトップレベルの要素名( channel )が無視されます。 生成されるJSONデータはレコード型で、 各属性および子要素に対してユニークなフィールドが含まれます。 ある要素が繰り返し現れる場合には配列へと変換されます:

1: 
2: 
3: 
4: 
{ "version": "1.0",
  "title": { "text": "Sample input" },
  "items": [ { "value": "First" }, 
             { "value": "Second" } ]  }

このように、 item 要素は自動的に items という複数形に変換されて、 value 属性の値に関連づけられた2つのレコード値が 配列の要素として含まれるようになります。

変換関数は XElement を引数にとり、 JsonValue を返すような再帰関数です。 この関数は( JsonValue.RecordJsonValue.Array を使って)JSONのレコードと 配列を組み立てます。 すべての属性は JsonValue.String に変換されます。 ただし今回の例では数値型を適切なJSON型に変換するような機能は実装しません:

 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: 
27: 
28: 
/// XML要素に対応するJSON表現を作成する
let rec fromXml (xml:XElement) =

  // すべての属性に対してキー値ペアのコレクションを作成する
  let attrs = 
    [ for attr in xml.Attributes() ->
        (attr.Name.LocalName, JsonValue.String attr.Value) ]

  // XElementのコレクションを(fromXmlを再帰的に使って)
  // JsonValueの配列に変換する関数
  let createArray xelems =
    [| for xelem in xelems -> fromXml xelem |]
    |> JsonValue.Array

  // 子要素を名前でグループ化した後、
  // 単一要素のグループを(再帰的に)レコードへと変換し、
  // 複数要素のグループをcreateArrayでJSON配列に変換する
  let children =
    xml.Elements() 
    |> Seq.groupBy (fun x -> x.Name.LocalName)
    |> Seq.map (fun (key, childs) ->
        match Seq.toList childs with
        | [child] -> key, fromXml child
        | children -> key + "s", createArray children )
        
  // 子要素および属性用に生成された要素を連結する
  Array.append (Array.ofList attrs) (Array.ofSeq children)
  |> JsonValue.Record

JSONからXMLへの変換

JSONからXMLへ変換する場合、同じようなミスマッチが起こります。 たとえば以下のようなJSONデータがあるとします:

1: 
2: 
3: 
{ "title" : "Sample input",
  "paging" : { "current": 1 },
  "items" : [ "First", "Second" ] }

トップレベルのレコードには名前がないため、 今回の変換機能では(ルート名を指定することになる)ユーザー側で XElement としてラップできるような XObject のリストを生成することにします。 レコード内にあるプリミティブな値のフィールドは属性になり、 (配列やレコードのような)複雑な値はオブジェクトになります:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
<root title="Sample input">
  <items>
    <item>First</item>
    <item>Second</item>
  </items>
  <paging current="1" />
</root>

変換関数はやはり再帰関数になります。 今回はパターンマッチを使って JsonValue のそれぞれ取り得るケースを区別します。 プリミティブ値を表すケースでは単に値を obj として返し、 配列やレコードに対してはネストされた(複数の)要素や属性を返します:

 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: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
/// JSONの値に対するXML表現を作成する
/// (ただしトップレベルの値がオブジェクトまたは配列の場合のみ機能する)
let toXml(x:JsonValue) =
  // XML属性やXML要素を作成するためのヘルパ関数
  let attr name value = 
    XAttribute(XName.Get name, value) :> XObject
  let elem name (value:obj) = 
    XElement(XName.Get name, value) :> XObject

  // 変換機能を実装している内部用再帰関数
  let rec toXml = function
    // Primitive values are returned as objects
    | JsonValue.Null -> null
    | JsonValue.Boolean b -> b :> obj
    | JsonValue.Number number -> number :> obj
    | JsonValue.Float number -> number :> obj
    | JsonValue.String s -> s :> obj

    // JSONオブジェクトは(プリミティブであれば)XML属性か、
    // あるいは子要素になる
    // attributes (for primitives) or child elements
    | JsonValue.Record properties -> 
      properties 
      |> Array.map (fun (key, value) ->
          match value with
          | JsonValue.String s -> attr key s
          | JsonValue.Boolean b -> attr key b
          | JsonValue.Number n -> attr key n
          | JsonValue.Float n -> attr key n
          | _ -> elem key (toXml value)) :> obj

    // JSON配列は <item> 要素のシーケンスになる
    | JsonValue.Array elements -> 
        elements |> Array.map (fun item -> 
          elem "item" (toXml item)) :> obj

  // 変換を実行して、結果をオブジェクトのシーケンスにキャストする
  // (意図しない入力に対しては失敗する可能性あり!)
  (toXml x) :?> XObject seq

関連する記事

namespace System
namespace System.Xml
namespace System.Xml.Linq
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
Multiple items
namespace FSharp.Data

--------------------
namespace Microsoft.FSharp.Data
val fromXml : xml:XElement -> JsonValue

Full name: JsonToXml.fromXml


 XML要素に対応するJSON表現を作成する
val xml : XElement
Multiple items
type XElement =
  inherit XContainer
  new : name:XName -> XElement + 4 overloads
  member AncestorsAndSelf : unit -> IEnumerable<XElement> + 1 overload
  member Attribute : name:XName -> XAttribute
  member Attributes : unit -> IEnumerable<XAttribute> + 1 overload
  member DescendantNodesAndSelf : unit -> IEnumerable<XNode>
  member DescendantsAndSelf : unit -> IEnumerable<XElement> + 1 overload
  member FirstAttribute : XAttribute
  member GetDefaultNamespace : unit -> XNamespace
  member GetNamespaceOfPrefix : prefix:string -> XNamespace
  member GetPrefixOfNamespace : ns:XNamespace -> string
  ...

Full name: System.Xml.Linq.XElement

--------------------
XElement(name: XName) : unit
XElement(other: XElement) : unit
XElement(other: XStreamingElement) : unit
XElement(name: XName, content: obj) : unit
XElement(name: XName, [<System.ParamArray>] content: obj []) : unit
val attrs : (string * JsonValue) list
val attr : XAttribute
XElement.Attributes() : System.Collections.Generic.IEnumerable<XAttribute>
XElement.Attributes(name: XName) : System.Collections.Generic.IEnumerable<XAttribute>
property XAttribute.Name: XName
property XName.LocalName: string
type JsonValue =
  | String of string
  | Number of decimal
  | Float of float
  | Record of properties: (string * JsonValue) []
  | Array of elements: JsonValue []
  | Boolean of bool
  | Null
  member Request : uri:string * ?httpMethod:string * ?headers:seq<string * string> -> HttpResponse
  member RequestAsync : uri:string * ?httpMethod:string * ?headers:seq<string * string> -> Async<HttpResponse>
  override ToString : unit -> string
  member ToString : saveOptions:JsonSaveOptions -> string
  member WriteTo : w:TextWriter * saveOptions:JsonSaveOptions -> unit
  static member AsyncLoad : uri:string * ?cultureInfo:CultureInfo -> Async<JsonValue>
  static member private JsonStringEncodeTo : w:TextWriter -> value:string -> unit
  static member Load : uri:string * ?cultureInfo:CultureInfo -> JsonValue
  static member Load : reader:TextReader * ?cultureInfo:CultureInfo -> JsonValue
  static member Load : stream:Stream * ?cultureInfo:CultureInfo -> JsonValue
  static member Parse : text:string * ?cultureInfo:CultureInfo -> JsonValue
  static member ParseMultiple : text:string * ?cultureInfo:CultureInfo -> seq<JsonValue>
  static member ParseSample : text:string * ?cultureInfo:CultureInfo -> JsonValue

Full name: FSharp.Data.JsonValue
union case JsonValue.String: string -> JsonValue
property XAttribute.Value: string
val createArray : (seq<#XElement> -> JsonValue)
val xelems : seq<#XElement>
val xelem : #XElement
union case JsonValue.Array: elements: JsonValue [] -> JsonValue
val children : seq<string * JsonValue>
XContainer.Elements() : System.Collections.Generic.IEnumerable<XElement>
XContainer.Elements(name: XName) : System.Collections.Generic.IEnumerable<XElement>
module Seq

from Microsoft.FSharp.Collections
val groupBy : projection:('T -> 'Key) -> source:seq<'T> -> seq<'Key * seq<'T>> (requires equality)

Full name: Microsoft.FSharp.Collections.Seq.groupBy
val x : XElement
property XElement.Name: XName
val map : mapping:('T -> 'U) -> source:seq<'T> -> seq<'U>

Full name: Microsoft.FSharp.Collections.Seq.map
val key : string
val childs : seq<XElement>
val toList : source:seq<'T> -> 'T list

Full name: Microsoft.FSharp.Collections.Seq.toList
val child : XElement
val children : XElement list
module Array

from Microsoft.FSharp.Collections
val append : array1:'T [] -> array2:'T [] -> 'T []

Full name: Microsoft.FSharp.Collections.Array.append
val ofList : list:'T list -> 'T []

Full name: Microsoft.FSharp.Collections.Array.ofList
val ofSeq : source:seq<'T> -> 'T []

Full name: Microsoft.FSharp.Collections.Array.ofSeq
union case JsonValue.Record: properties: (string * JsonValue) [] -> JsonValue
val toXml : x:JsonValue -> seq<XObject>

Full name: JsonToXml.toXml


 JSONの値に対するXML表現を作成する
 (ただしトップレベルの値がオブジェクトまたは配列の場合のみ機能する)
val x : JsonValue
val attr : (string -> 'a -> XObject)
val name : string
val value : 'a
Multiple items
type XAttribute =
  inherit XObject
  new : other:XAttribute -> XAttribute + 1 overload
  member IsNamespaceDeclaration : bool
  member Name : XName
  member NextAttribute : XAttribute
  member NodeType : XmlNodeType
  member PreviousAttribute : XAttribute
  member Remove : unit -> unit
  member SetValue : value:obj -> unit
  member ToString : unit -> string
  member Value : string with get, set
  ...

Full name: System.Xml.Linq.XAttribute

--------------------
XAttribute(other: XAttribute) : unit
XAttribute(name: XName, value: obj) : unit
type XName =
  member Equals : obj:obj -> bool
  member GetHashCode : unit -> int
  member LocalName : string
  member Namespace : XNamespace
  member NamespaceName : string
  member ToString : unit -> string
  static member Get : expandedName:string -> XName + 1 overload

Full name: System.Xml.Linq.XName
XName.Get(expandedName: string) : XName
XName.Get(localName: string, namespaceName: string) : XName
type XObject =
  member AddAnnotation : annotation:obj -> unit
  member Annotation<'T> : unit -> 'T + 1 overload
  member Annotations<'T> : unit -> IEnumerable<'T> + 1 overload
  member BaseUri : string
  member Document : XDocument
  member NodeType : XmlNodeType
  member Parent : XElement
  member RemoveAnnotations<'T> : unit -> unit + 1 overload
  event Changed : EventHandler<XObjectChangeEventArgs>
  event Changing : EventHandler<XObjectChangeEventArgs>

Full name: System.Xml.Linq.XObject
val elem : (string -> obj -> XObject)
val value : obj
type obj = System.Object

Full name: Microsoft.FSharp.Core.obj
val toXml : (JsonValue -> obj)
union case JsonValue.Null: JsonValue
union case JsonValue.Boolean: bool -> JsonValue
val b : bool
union case JsonValue.Number: decimal -> JsonValue
val number : decimal
union case JsonValue.Float: float -> JsonValue
val number : float
val s : string
val properties : (string * JsonValue) []
val map : mapping:('T -> 'U) -> array:'T [] -> 'U []

Full name: Microsoft.FSharp.Collections.Array.map
val value : JsonValue
val n : decimal
val n : float
val elements : JsonValue []
val item : JsonValue
Multiple items
val seq : sequence:seq<'T> -> seq<'T>

Full name: Microsoft.FSharp.Core.Operators.seq

--------------------
type seq<'T> = System.Collections.Generic.IEnumerable<'T>

Full name: Microsoft.FSharp.Collections.seq<_>
Fork me on GitHub