FSharp.Data


Converting between JSON and XML

BinderScriptNotebook

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 FSharp.Data package, 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 FSharp.Data package, 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:

#r "System.Xml.Linq.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:

/// 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:

/// 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
Multiple items
namespace FSharp

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

--------------------
namespace Microsoft.FSharp.Data
val fromXml: xml: XElement -> JsonValue
 Creates a JSON representation of a XML element
val xml: XElement
Multiple items
type XElement = inherit XContainer interface IXmlSerializable new: other: XElement -> unit + 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 GetDefaultNamespace: unit -> XNamespace member GetNamespaceOfPrefix: prefix: string -> XNamespace ...
<summary>Represents an XML element. See XElement Class Overview and the Remarks section on this page for usage information and examples.</summary>

--------------------
XElement(other: XElement) : XElement
XElement(name: XName) : XElement
XElement(other: XStreamingElement) : XElement
XElement(name: XName, content: obj) : XElement
XElement(name: XName, [<System.ParamArray>] content: obj[]) : XElement
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 with get
<summary>Gets the expanded name of this attribute.</summary>
<returns>An <see cref="T:System.Xml.Linq.XName" /> containing the name of this attribute.</returns>
property XName.LocalName: string with get
<summary>Gets the local (unqualified) part of the name.</summary>
<returns>A <see cref="T:System.String" /> that contains the local (unqualified) part of the name.</returns>
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: url: string * ?httpMethod: string * ?headers: seq<string * string> -> HttpResponse member RequestAsync: url: string * ?httpMethod: string * ?headers: seq<string * string> -> Async<HttpResponse> member ToString: saveOptions: JsonSaveOptions -> string + 1 overload member WriteTo: w: TextWriter * saveOptions: JsonSaveOptions -> unit static member AsyncLoad: uri: string * ?encoding: Encoding -> Async<JsonValue> static member Load: stream: Stream -> JsonValue + 2 overloads static member Parse: text: string -> JsonValue static member ParseMultiple: text: string -> seq<JsonValue> static member TryParse: text: string -> JsonValue option
<summary> Represents a JSON value. Large numbers that do not fit in the Decimal type are represented using the Float case, while smaller numbers are represented as decimals to avoid precision loss. </summary>
union case JsonValue.String: string -> JsonValue
property XAttribute.Value: string with get, set
<summary>Gets or sets the value of this attribute.</summary>
<exception cref="T:System.ArgumentNullException">When setting, the <paramref name="value" /> is <see langword="null" />.</exception>
<returns>A <see cref="T:System.String" /> containing the value of this attribute.</returns>
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
<summary>Contains operations for working with values of type <see cref="T:Microsoft.FSharp.Collections.seq`1" />.</summary>
val groupBy: projection: ('T -> 'Key) -> source: seq<'T> -> seq<'Key * seq<'T>> (requires equality)
<summary>Applies a key-generating function to each element of a sequence and yields a sequence of unique keys. Each unique key contains a sequence of all elements that match to this key.</summary>
<remarks>This function returns a sequence that digests the whole initial sequence as soon as that sequence is iterated. As a result this function should not be used with large or infinite sequences. The function makes no assumption on the ordering of the original sequence.</remarks>
<param name="projection">A function that transforms an element of the sequence into a comparable key.</param>
<param name="source">The input sequence.</param>
<returns>The result sequence.</returns>
<example id="group-by-1"><code lang="fsharp"> let inputs = [1; 2; 3; 4; 5] inputs |&gt; Seq.groupBy (fun n -&gt; n % 2) </code> Evaluates to a sequence yielding the same results as <c>seq { (1, seq { 1; 3; 5 }); (0, seq { 2; 4 }) }</c></example>
val x: XElement
property XElement.Name: XName with get, set
<summary>Gets or sets the name of this element.</summary>
<returns>An <see cref="T:System.Xml.Linq.XName" /> that contains the name of this element.</returns>
val map: mapping: ('T -> 'U) -> source: seq<'T> -> seq<'U>
<summary>Builds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The given function will be applied as elements are demanded using the <c>MoveNext</c> method on enumerators retrieved from the object.</summary>
<remarks>The returned sequence may be passed between threads safely. However, individual IEnumerator values generated from the returned sequence should not be accessed concurrently.</remarks>
<param name="mapping">A function to transform items from the input sequence.</param>
<param name="source">The input sequence.</param>
<returns>The result sequence.</returns>
<exception cref="T:System.ArgumentNullException">Thrown when the input sequence is null.</exception>
<example id="item-1"><code lang="fsharp"> let inputs = ["a"; "bbb"; "cc"] inputs |&gt; Seq.map (fun x -&gt; x.Length) </code> Evaluates to a sequence yielding the same results as <c>seq { 1; 3; 2 }</c></example>
val key: string
val childs: seq<XElement>
val toList: source: seq<'T> -> 'T list
<summary>Builds a list from the given collection.</summary>
<param name="source">The input sequence.</param>
<returns>The result list.</returns>
<exception cref="T:System.ArgumentNullException">Thrown when the input sequence is null.</exception>
<example id="tolist-1"><code lang="fsharp"> let inputs = seq { 1; 2; 5 } inputs |&gt; Seq.toList </code> Evaluates to <c>[ 1; 2; 5 ]</c>. </example>
val child: XElement
val children: XElement list
module Array from Microsoft.FSharp.Collections
<summary>Contains operations for working with arrays.</summary>
<remarks> See also <a href="https://docs.microsoft.com/dotnet/fsharp/language-reference/arrays">F# Language Guide - Arrays</a>. </remarks>
val append: array1: 'T[] -> array2: 'T[] -> 'T[]
<summary>Builds a new array that contains the elements of the first array followed by the elements of the second array.</summary>
<param name="array1">The first input array.</param>
<param name="array2">The second input array.</param>
<returns>The resulting array.</returns>
<exception cref="T:System.ArgumentNullException">Thrown when either of the input arrays is null.</exception>
<example id="append-1"><code lang="fsharp"> Array.append [| 1; 2 |] [| 3; 4 |] </code> Evaluates to <c>[| 1; 2; 3; 4 |]</c>. </example>
val ofList: list: 'T list -> 'T[]
<summary>Builds an array from the given list.</summary>
<param name="list">The input list.</param>
<returns>The array of elements from the list.</returns>
<example id="oflist-1"><code lang="fsharp"> let inputs = [ 1; 2; 5 ] inputs |&gt; Array.ofList </code> Evaluates to <c>[| 1; 2; 5 |]</c>. </example>
val ofSeq: source: seq<'T> -> 'T[]
<summary>Builds a new array from the given enumerable object.</summary>
<param name="source">The input sequence.</param>
<returns>The array of elements from the sequence.</returns>
<exception cref="T:System.ArgumentNullException">Thrown when the input sequence is null.</exception>
<example id="ofseq-1"><code lang="fsharp"> let inputs = seq { 1; 2; 5 } inputs |&gt; Array.ofSeq </code> Evaluates to <c>[| 1; 2; 5 |]</c>. </example>
union case JsonValue.Record: properties: (string * JsonValue)[] -> JsonValue
val toXml: x: JsonValue -> seq<XObject>
 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 -> unit + 1 overload member Remove: unit -> unit member SetValue: value: obj -> unit member ToString: unit -> string static member op_Explicit: attribute: XAttribute -> bool + 24 overloads member IsNamespaceDeclaration: bool member Name: XName member NextAttribute: XAttribute member NodeType: XmlNodeType ...
<summary>Represents an XML attribute.</summary>

--------------------
XAttribute(other: XAttribute) : XAttribute
XAttribute(name: XName, value: obj) : XAttribute
type XName = interface IEquatable<XName> interface ISerializable member Equals: obj: obj -> bool member GetHashCode: unit -> int member ToString: unit -> string static member (<>) : left: XName * right: XName -> bool static member (=) : left: XName * right: XName -> bool static member Get: expandedName: string -> XName + 1 overload static member op_Implicit: expandedName: string -> XName member LocalName: string ...
<summary>Represents a name of an XML element or attribute.</summary>
XName.Get(expandedName: string) : XName
XName.Get(localName: string, namespaceName: string) : XName
type XObject = interface IXmlLineInfo member AddAnnotation: annotation: obj -> unit member Annotation: ``type`` : Type -> obj + 1 overload member Annotations: ``type`` : Type -> IEnumerable<obj> + 1 overload member RemoveAnnotations: ``type`` : Type -> unit + 1 overload member BaseUri: string member Document: XDocument member NodeType: XmlNodeType member Parent: XElement member Changed: EventHandler<XObjectChangeEventArgs> ...
<summary>Represents a node or an attribute in an XML tree.</summary>
val elem: (string -> obj -> XObject)
val value: obj
type obj = System.Object
<summary>An abbreviation for the CLI type <see cref="T:System.Object" />.</summary>
<category>Basic Types</category>
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[]
<summary>Builds a new array whose elements are the results of applying the given function to each of the elements of the array.</summary>
<param name="mapping">The function to transform elements of the array.</param>
<param name="array">The input array.</param>
<returns>The array of transformed elements.</returns>
<exception cref="T:System.ArgumentNullException">Thrown when the input array is null.</exception>
<example id="map-1"><code lang="fsharp"> let inputs = [| "a"; "bbb"; "cc" |] inputs |&gt; Array.map (fun x -&gt; x.Length) </code> Evaluates to <c>[| 1; 3; 2 |]</c></example>
val value: JsonValue
val n: decimal
val n: float
val elements: JsonValue[]
val item: JsonValue
Multiple items
val seq: sequence: seq<'T> -> seq<'T>
<summary>Builds a sequence using sequence expression syntax</summary>
<param name="sequence">The input sequence.</param>
<returns>The result sequence.</returns>
<example id="seq-cast-example"><code lang="fsharp"> seq { for i in 0..10 do yield (i, i*i) } </code></example>


--------------------
type seq<'T> = System.Collections.Generic.IEnumerable<'T>
<summary>An abbreviation for the CLI type <see cref="T:System.Collections.Generic.IEnumerable`1" /></summary>
<remarks> See the <see cref="T:Microsoft.FSharp.Collections.SeqModule" /> module for further operations related to sequences. See also <a href="https://docs.microsoft.com/dotnet/fsharp/language-reference/sequences">F# Language Guide - Sequences</a>. </remarks>