Expressions rendered as both XML and JSON
1 Rationale
2 A more realistic example
3 Basics
flexpr?
3.1 XML
flexpr->xexpr
write-flexpr-xml/  content
display-flexpr-xml/  content
3.2 JSON
flexpr->jsexpr
write-flexpr-json
4 Customizing "plural" symbols
default-singular-symbol
current-singular-symbol
singular-symbol/  c
plural-symbol?
6.12

Expressions rendered as both XML and JSON

 (require flexpr) package: flexpr

1 Rationale

A flexpr? is like a xexpr? or jsexpr?, but more flexible. A flexpr? can be sensibly converted to either XML or JSON.

Example use case: Your web service wants to offer the same response data in the client’s choice of XML or JSON (as expressed by the client in a URI query parameter or Accept header). Early in your response pipeline, define your response as a flexpr?. Convert to the desired format as/when needed.

A small benefit: A flexpr? doesn’t make you prematurely represent numbers as strings. They can remain numbers for JSON. They’re converted to strings only for XML. This makes it a bit more convenient to write flexpr?s by hand. Unlike xexpr?s you don’t need to use ~a on non-string values.

So what should a flexpr? be?

It’s tempting to start with an xexpr?. However the general form of XML is awkward to convert to JSON satisfactorily. An XML element essentially has two, parallel key/value dictionaries: the attributes and the child elements. A JSON dict would need to store the children in some sub-dict with a magic key name like say "body" (or equally weirdly, store the attributes in a sub-dict).

Another reason to prefer child elements is that attribute values are limited to primitives like strings, numbers, and booleans; there’s no arbitrary nesting.

Let’s back up. Let’s stipulate that having a "fork" of two dicts at the same level is weird. It’s weird even for XML. In fact most web service response XML I’ve seen doesn’t use attributes, just child elements.

Instead let’s start with a jsexpr?. A flexpr? is a slightly restricted form of jsexpr?:

  flexpr = boolean?
  | string?
  | exact-integer?
  | inexact-real?
  | (hasheq key-val ...)
     
  key-val = [symbol? flexpr]
  | [plural-symbol? (list flexpr ...)]

Given this definition:

2 A more realistic example

> (define v (hasheq 'ResponseId 123123
                    'Students
                    (list (hasheq 'FirstName "John"
                                  'LastName "Doe"
                                  'Age 12
                                  'Active #f
                                  'GPA 3.4)
                          (hasheq 'FirstName "Alyssa"
                                  'LastName "Hacker"
                                  'Age 14
                                  'Active #t
                                  'GPA 4.0))))
> (flexpr? v)

#t

> (pretty-print (flexpr->xexpr v))

'(Response

  ()

  (ResponseId () "123123")

  (Students

   ()

   (Student

    ()

    (Active () "false")

    (Age () "12")

    (FirstName () "John")

    (GPA () "3.4")

    (LastName () "Doe"))

   (Student

    ()

    (Active () "true")

    (Age () "14")

    (FirstName () "Alyssa")

    (GPA () "4.0")

    (LastName () "Hacker"))))

> (display-xml/content (xexpr->xml (flexpr->xexpr v)))

<Response>

  <ResponseId>

    123123

  </ResponseId>

  <Students>

    <Student>

      <Active>

        false

      </Active>

      <Age>

        12

      </Age>

      <FirstName>

        John

      </FirstName>

      <GPA>

        3.4

      </GPA>

      <LastName>

        Doe

      </LastName>

    </Student>

    <Student>

      <Active>

        true

      </Active>

      <Age>

        14

      </Age>

      <FirstName>

        Alyssa

      </FirstName>

      <GPA>

        4.0

      </GPA>

      <LastName>

        Hacker

      </LastName>

    </Student>

  </Students>

</Response>

> (pretty-print (flexpr->jsexpr v))

'#hasheq((Students

          .

          (#hasheq((Age . 12)

                   (Active . #f)

                   (GPA . 3.4)

                   (LastName . "Doe")

                   (FirstName . "John"))

           #hasheq((Age . 14)

                   (Active . #t)

                   (GPA . 4.0)

                   (LastName . "Hacker")

                   (FirstName . "Alyssa"))))

         (ResponseId . 123123))

> (write-json (flexpr->jsexpr v))

{"Students":[{"Age":12,"Active":false,"GPA":3.4,"LastName":"Doe","FirstName":"John"},{"Age":14,"Active":true,"GPA":4.0,"LastName":"Hacker","FirstName":"Alyssa"}],"ResponseId":123123}

3 Basics

procedure

(flexpr? v)  boolean?

  v : any/c
Predicate.

3.1 XML

procedure

(flexpr->xexpr v [#:root root])  xexpr?

  v : flexpr?
  root : symbol? = 'Response
Convert v to an xexpr?, which is enclosed in an element whose tag is root.

While traversing v, flexpr->xexpr performs equivalent tests as flexpr?, but will provide an error message specific to the item that failed. For example when a plural key is required for a (listof flexpr?) value, the error message is:

> (flexpr->xexpr (hasheq 'Item (list 0 1)))

flexpr->xexpr: hash table key must be plural-symbol?

  expected: 'Items

  given: 'Item

  in: '#hasheq((Item . (0 1)))

providing a hint that instead you should do:

> (flexpr->xexpr (hasheq 'Items (list 0 1)))

'(Response () (Items () (Item () "0") (Item () "1")))

procedure

(write-flexpr-xml/content v [out #:root root])  void?

  v : flexpr?
  out : output-port? = (current-output-port)
  root : symbol? = 'Response
Write v as XML.

Effectively the composition of write-xml/content and flexpr->xexpr.

procedure

(display-flexpr-xml/content v    
  [out    
  #:root root])  void?
  v : flexpr?
  out : output-port? = (current-output-port)
  root : symbol? = 'Response
Write v as XML.

Effectively the composition of display-xml/content and flexpr->xexpr.

3.2 JSON

procedure

(flexpr->jsexpr v)  jsexpr?

  v : flexpr?
Convert v to a jsexpr?.

Because a flexpr? is a subset of a jsexpr?, this is approximately the composition of values with flexpr?.

procedure

(write-flexpr-json v    
  [out    
  #:null jsnull    
  #:encode encode])  void?
  v : flexpr?
  out : output-port? = (current-output-port)
  jsnull : any/c = (json-null)
  encode : (or/c 'control 'all) = 'control
Write v as JSON.

Effectively the composition of write-json and flexpr->jsexpr.

4 Customizing "plural" symbols

Although the default idea of a "plural symbol" is simply default-singular-symbol, you may enhance this by supplying a different function as the value of the current-singular-symbol parameter.

procedure

(default-singular-symbol v)  (or/c symbol? #f)

  v : symbol?
The default value of the current-singular-symbol parameter – a function that simply converts a symbol ending in #\s to a symbol lacking the #\s, and returns #f for all other symbols.

> (default-singular-symbol 'vampires)

'vampire

> (default-singular-symbol 'vampire)

#f

A parameter whose value is a function that converts plural symbols to singular, and returns #f for all other symbols.

You may customize this to extend or replace default-singular-symbol.

> (parameterize ([current-singular-symbol
                  (λ (s)
                    (or (and (eq? s 'Werewolves) 'Werewolf)
                        (default-singular-symbol s)))])
    (pretty-print
     (flexpr->xexpr
      (hasheq 'Werewolves
              (list (hasheq 'FirstName "John"
                            'FangLength 6.4)
                    (hasheq 'FirstName "Alyssa"
                            'FangLength 5.0))
              'Vampires
              (list (hasheq 'FirstName "John"
                            'FangLength 3.4)
                    (hasheq 'FirstName "Alyssa"
                            'FangLength 4.0))))))

'(Response

  ()

  (Vampires

   ()

   (Vampire () (FangLength () "3.4") (FirstName () "John"))

   (Vampire () (FangLength () "4.0") (FirstName () "Alyssa")))

  (Werewolves

   ()

   (Werewolf () (FangLength () "6.4") (FirstName () "John"))

   (Werewolf () (FangLength () "5.0") (FirstName () "Alyssa"))))

procedure

(plural-symbol? v)  boolean?

  v : any/c
A predicate that is effectively just:

(and ((current-singular-symbol) v) #t)