F# Data


F# Data: Converting between JSON and XML

This tutorial shows how to implement convert JSON document (represented using the JsonValue type discussed in JSON parser article) to an XML document (represented as XElement) and the other way round. This functionality is not directly available in the F# Data library, but it can be very easily implemented by recursively walking over the JSON (or XML) document.

If you want to use the JSON to/from XML conversion in your code, you can copy the source from GitHub and just include it in your project. If you use these functions often and would like to see them in the F# Data library, please submit a feature request.

Initialization

We will be using the LINQ to XML API (available in System.Xml.Linq.dll) and the JsonValue which is available in the FSharp.Data namespace:

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

In this script, we create a conversion that returns an easy to process value, but the conversion is not reversible (e.g. converting JSON to XML and then back to JSON will produce a different value).

Converting XML to JSON

Although XML and JSON are quite similar formats, there is a number of subtle differences. In particular, XML distinguishes between attributes and child elements. Moreover, all XML elements have a name, while JSON arrays or records are anonymous (but records have named fields). Consider, for example, the following XML:

<channel version="1.0">
  <title text="Sample input" />
  <item value="First" />
  <item value="Second" />
</channel>

The JSON that we produce will ignore the top-level element name (channel). It produces a record that contains a unique field for every attribute and a name of a child element. If an element appears multiple times, it is turned into an array:

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

As you can see, the item element has been automatically pluralized to items and the array contains two record values that consist of the value attribute.

The conversion function is a recursive function that takes a XElement and produces JsonValue. It builds JSON records (using JsonValue.Record) and arrays (using JsonValue.Array). All attribute values are turned into JsonValue.String - the sample does not imlement more sophisticated conversion that would turn numeric attributes to a corresponding JSON type:

 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: 
/// Creates a JSON representation of a XML element
let rec fromXml (xml:XElement) =

  // Create a collection of key/value pairs for all attributes      
  let attrs = 
    [ for attr in xml.Attributes() ->
        (attr.Name.LocalName, JsonValue.String attr.Value) ]

  // Function that turns a collection of XElement values
  // into an array of JsonValue (using fromXml recursively)
  let createArray xelems =
    [| for xelem in xelems -> fromXml xelem |]
    |> JsonValue.Array

  // Group child elements by their name and then turn all single-
  // element groups into a record (recursively) and all multi-
  // element groups into a JSON array using createArray
  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 )
        
  // Concatenate elements produced for child elements & attributes
  Array.append (Array.ofList attrs) (Array.ofSeq children)
  |> JsonValue.Record

Converting JSON to XML

When converting JSON value to XML, we fact the same mismatch. Consider the following JSON value:

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

The top-level record does not have a name, so our conversion produces a list of XObject values that can be wrapped into an XElement by the user (who has to specify the root name). Record fields that are a primitive value are turned into attributes, while complex values (array or record) become objects:

<root title="Sample input">
  <items>
    <item>First</item>
    <item>Second</item>
  </items>
  <paging current="1" />
</root>

The conversion function is, again, implemented as a recursive function. This time, we use pattern matching to distinguish between the different possible cases of JsonValue. The cases representing a primitive value simply return the value as obj, while array and record construct nested element(s) or attribute:

 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: 
40: 
/// Creates an XML representation of a JSON value (works 
/// only when the top-level value is an object or an array)
let toXml(x:JsonValue) =
  // Helper functions for constructing XML 
  // attributes and XML elements
  let attr name value = 
    XAttribute(XName.Get name, value) :> XObject
  let elem name (value:obj) = 
    XElement(XName.Get name, value) :> XObject

  // Inner recursive function that implements the conversion
  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 object becomes a collection of 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 array is turned into a 
    // sequence of <item> elements
    | JsonValue.Array elements -> 
        elements |> Array.map (fun item -> 
          elem "item" (toXml item)) :> obj

  // Perform the conversion and cast the result to sequence
  // of objects (may fail for unexpected inputs!)
  (toXml x) :?> XObject seq

Related articles

namespace System
namespace System.Xml
namespace System.Xml.Linq
namespace FSharp
namespace FSharp.Data
val fromXml : xml:XElement -> JsonValue

Full name: JsonToXml.fromXml


 Creates a JSON representation of a XML element
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, params 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 Post : uri:string * ?headers:(string * string) list -> HttpResponse
  member Request : uri:string * ?httpMethod:string * ?headers:(string * string) list -> HttpResponse
  member RequestAsync : uri:string * ?httpMethod:string * ?headers:(string * string) list -> Async<HttpResponse>
  override ToString : unit -> string
  member ToString : saveOptions:JsonSaveOptions -> string
  static member AsyncLoad : uri:string * ?cultureInfo:CultureInfo -> Async<JsonValue>
  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


 Creates an XML representation of a JSON value (works
 only when the top-level value is an object or an array)
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