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
- F# Data: JSON Parser and Reader - a tutorial that introduces
JsonValue
for working with JSON values dynamically. - F# Data: JSON Type Provider - discusses F# type provider that provides type-safe access to JSON data.
- F# Data: XML Type Provider - discusses the F# type provider that provides type-safe access to XML data.
Full name: JsonToXml.fromXml
Creates a JSON representation of a XML element
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
XElement.Attributes(name: XName) : System.Collections.Generic.IEnumerable<XAttribute>
| 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
XContainer.Elements(name: XName) : System.Collections.Generic.IEnumerable<XElement>
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.groupBy
Full name: Microsoft.FSharp.Collections.Seq.map
Full name: Microsoft.FSharp.Collections.Seq.toList
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Array.append
Full name: Microsoft.FSharp.Collections.Array.ofList
Full name: Microsoft.FSharp.Collections.Array.ofSeq
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)
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
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(localName: string, namespaceName: string) : XName
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
Full name: Microsoft.FSharp.Core.obj
Full name: Microsoft.FSharp.Collections.Array.map
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<_>