build JSON with tdom

Whatbuild JSON with tdom using createNodeCmd
Requirestdom 0.9.2
tagstdom
referencehttp://www.tdom.org/index.html/doc/trunk/doc/index.html
Updated03.07.2022

Index

  1. Challenge
  2. insufficient Approach
  3. Issues and Improvements
  4. improved Approvements
  5. Summary
  6. Discussion

Challenge

how to build a JSON like this:

{
    "aNumber": 0.123,
    "aNumberAsString": "0.123",
    "aString": "this is a String",
    "a key with spaces": "please take care on setting: dom setNameCheck 0",
    "aArray": ["a","b","c"],   
    "aObject": {
        "foo":"that",
        "bar":"grill"
    },
    "aObjectArray": [
        {
            "foo": "objArray_01_foo",
            "bar": "objArray_01_bar",
            "array": ["five_01_a", "five_01_b", "five_01_c"]
        },
        {
            "foo": "objArray_02_foo",
            "bar": "objArray_02_bar",
            "array": ["five_02_a", "five_02_b", "five_02_c"]
        }
    ] 
}

insufficient Approach

    #
    # --- load tdom ---
    #
package require tdom 0.9    ;# current version: 0.9.2 (May, 2022)
puts "tdom: [package versions tdom]"
    #
    # http://www.tdom.org/index.html/file?name=tests/domjson.test&ci=release
    #
    #
set doc [dom createDocumentNode root]
    #
puts "asXML:  [$root asXML  -indent 4]"
puts "asJSON: [$root asJSON -indent 4]"
    #
    # -- aNumber --
    #
set node_Number     [$doc createElement "aNumber"]
$node_Number appendChild [$doc createTextNode 0.123]
$root appendChild $node_Number
    #
    # -- aNumberAsString --
    #
set node_StringNum  [$doc createElement "aNumberAsString"]
$node_StringNum appendChild [$doc createTextNode 0.123]
$root appendChild $node_StringNum
    #
    # -- aString --
    #
set node_String     [$doc createElement "aNumberString"]
$node_String appendChild [$doc createTextNode "0.123"]
$root appendChild $node_String
    #
set node_String     [$doc createElement "aString"]
$node_String appendChild [$doc createTextNode "this is a String"]
$root appendChild $node_String
    #
    # -- a key with spaces --
    #
    # ... you have to disable namecheck first
dom setNameCheck 0
set node_String     [$doc createElement "a key with spaces"]
    # [dom setNameCheck] == 1  ... brings this error
    #       ERROR: Invalid tag name 'a key with spaces'
    #         while executing
    #       "$doc createElement "a key with spaces""
    #         invoked from within
    #       "set  node_String     [$doc createElement "a key with spaces"]"
    #
$node_String appendChild [$doc createTextNode "please take care on setting: dom setNameCheck 0"]
$root appendChild $node_String
    #
    # -- aArray --
    #
set node_Array      [$doc createElement "aArray"]
$node_Array appendChild [$doc createTextNode "a"]
$node_Array appendChild [$doc createTextNode "b"]
$node_Array appendChild [$doc createTextNode "c"]
$root appendChild $node_Array
    #
    #
    # -- aObject --
    #
set node_Object     [$doc createElement "aObject"]
    #
set node__Label     [$doc createElement "foo"]
set node__Value     [$doc createTextNode "that"]
$node_Object appendChild $node__Label
$node__Label appendChild $node__Value
    #
set node__Label     [$doc createElement "bar"]
set node__Value     [$doc createTextNode "grill"]
$node_Object appendChild $node__Label
$node__Label appendChild $node__Value
    #
$root appendChild $node_Object
    #
    #
    # -- aObjectArray --
    #
set node_ObjArray   [$doc createElement "aObjectArray"]
    #
set node__Obj_01    [$doc createElement "obj0001"]
    #
set node__Label     [$doc createElement "foo"]
set node__Value     [$doc createTextNode "objArray_01_foo"]
$node__Label  appendChild $node__Value
$node__Obj_01 appendChild $node__Label
set node__Label     [$doc createElement "bar"]
set node__Value     [$doc createTextNode "objArray_01_bar"]
$node__Label  appendChild $node__Value
$node__Obj_01 appendChild $node__Label
    #
set node__Array     [$doc createElement "array"]
$node__Array appendChild [$doc createTextNode "five_01_a"]
$node__Array appendChild [$doc createTextNode "five_01_b"]
$node__Array appendChild [$doc createTextNode "five_01_c"]
    #
$node__Obj_01 appendChild $node__Array   
    #
    #
set node__Obj_02    [$doc createElement "obj0002"]
    #
set node__Label     [$doc createElement "foo"]
set node__Value     [$doc createTextNode "objArray_02_foo"]
$node__Label  appendChild $node__Value
$node__Obj_02 appendChild $node__Label
set node__Label     [$doc createElement "bar"]
set node__Value     [$doc createTextNode "objArray_02_bar"]
$node__Label  appendChild $node__Value
$node__Obj_02 appendChild $node__Label
    #
set node__Array     [$doc createElement "array" ARRAY]
$node__Array appendChild [$doc createTextNode "five_02_a"]
$node__Array appendChild [$doc createTextNode "five_02_b"]
$node__Array appendChild [$doc createTextNode "five_02_c"]
    #
$node__Obj_02 appendChild $node__Array
    #
$node_ObjArray appendChild $node__Obj_01
$node_ObjArray appendChild $node__Obj_02
    #
    #
$root appendChild $node_ObjArray
    #
    # in the case of mixed OBJECT and ARRAYs you have to update the specific rendering
$node_ObjArray jsonType ARRAY    
$node__Obj_01  jsonType OBJECT   
$node__Obj_02  jsonType OBJECT   
    #
puts "== \$resultJSON ===="
puts [$doc asXML  -indent 4]
puts "== \$resultJSON ===="
puts [$doc asJSON -indent 4]
    #

... output of this script:

tdom: 0.9.2
asXML:
asJSON: {}
== $resultJSON ====
<aNumber>0.123</aNumber>
<aNumberAsString>0.123</aNumberAsString>
<aNumberString>0.123</aNumberString>
<aString>this is a String</aString>
<a key with spaces>please take care on setting: dom setNameCheck 0</a key with spaces>
<aArray>abc</aArray>
<aObject>
    <foo>that</foo>
    <bar>grill</bar>
</aObject>
<aObjectArray>
    <obj0001>
        <foo>objArray_01_foo</foo>
        <bar>objArray_01_bar</bar>
        <array>five_01_afive_01_bfive_01_c</array>
    </obj0001>
    <obj0002>
        <foo>objArray_02_foo</foo>
        <bar>objArray_02_bar</bar>
        <array>five_02_afive_02_bfive_02_c</array>
    </obj0002>
</aObjectArray>

== $resultJSON ====
{
    "aNumber": "0.123",
    "aNumberAsString": "0.123",
    "aNumberString": "0.123",
    "aString": "this is a String",
    "a key with spaces": "please take care on setting: dom setNameCheck 0",
    "aArray": [
        "a",
        "b",
        "c"
    ],
    "aObject": {
        "foo": "that",
        "bar": "grill"
    },
    "aObjectArray": [
        {
            "foo": "objArray_01_foo",
            "bar": "objArray_01_bar",
            "array": [
                "five_01_a",
                "five_01_b",
                "five_01_c"
            ]
        },
        {
            "foo": "objArray_02_foo",
            "bar": "objArray_02_bar",
            "array": [
                "five_02_a",
                "five_02_b",
                "five_02_c"
            ]
        }
    ]
}

Issues and Imoprovements

wrong Value

this Result: "aNumber": "0.123"
expected Result: "aNumber": 0.123

complex code

... the given approach takes a lot of lines to create this result

improved Approach

use

dom createNodeCmd 

to improve our code

    #
    # --- load tdom ---
    #
package require tdom 0.9    ;# current version: 0.9.2 (May, 2022)
puts "tdom: [package versions tdom]"
    #
    # http://www.tdom.org/index.html/file?name=tests/domjson.test&ci=release
    #
    #    
    #
puts "== 01 == \$resultJSON ===="
set doc [dom createDocumentNode root]
    #
puts "asXML:  [$root asXML  -indent 4]"
puts "asJSON: [$root asJSON -indent 4]"
    #
    #
    # our targetJSON includes nodes with predefined names: 
    #       "aNumber" "aString" "a key with spaces" "foo" "bar"
    #
    #   ... lets predefine dom-commands to create these nodes
    #
dom createNodeCmd -jsonType NONE    elementNode aNumber
dom createNodeCmd -jsonType NONE    elementNode aNumberAsString
dom createNodeCmd -jsonType NONE    elementNode aString
dom createNodeCmd -jsonType NONE    elementNode "a key with spaces"
dom createNodeCmd -jsonType NONE    elementNode foo
dom createNodeCmd -jsonType NONE    elementNode bar
dom createNodeCmd -jsonType NONE    elementNode array
dom createNodeCmd -jsonType ARRAY   elementNode "aArray"
    #
    # ... json distinguishs between 
    #     ... Arrays  ["A","B","C"] and 
    #     ... Objects {"a": "A", "b": "B", "c":"C"}
    #
dom createNodeCmd -jsonType OBJECT  elementNode "aObject"
dom createNodeCmd -jsonType ARRAY   elementNode "aObjectArray"
    #
    # ... json distinguishs between 
    #     ... Strings  "0.123" 
    #     ... Numbers   0.123
    #
dom createNodeCmd -jsonType NUMBER  textNode    jsonNumber
dom createNodeCmd -jsonType STRING  textNode    jsonString    
    #
    #
set resultJSON [dom createDocumentNode]
    #
puts "== 02 == \$resultJSON ===="
puts [$resultJSON asJSON -indent 4]    
    #
$resultJSON appendFromScript {
    aNumber {jsonNumber 0.123}
    aNumberAsString {jsonString 0.123}
    aString {jsonString "this is a string"}
    "a key with spaces" {jsonString "It's possible."}
    aArray {
        jsonString a
        jsonString b
        jsonString c
    }
    aObject {
        foo {jsonString "that"}
        bar {jsonString "grill"}
    }
    aObjectArray {
        aObject {
            foo {jsonString "objArray_01_foo"}
            bar {jsonString "objArray_01_bar"}
            aArray {
                jsonString "five_01_a"
                jsonString "five_01_b"
                jsonString "five_01_c"
            }
        }
        aObject {
            foo {jsonString "objArray_02_foo"}
            bar {jsonString "objArray_02_bar"}
            aArray {
                jsonString "five_02_a"
                jsonString "five_02_b"
                jsonString "five_02_c"
            }
        }
    }
}
    #
    #
puts "== 03 == \$resultJSON ===="
puts [$resultJSON asXML  -indent 4]
puts "== 03 == \$resultJSON == final =="
puts [$resultJSON asJSON -indent 4]    
    #

... output of this script:

tdom: 0.9.2
== 01 == $resultJSON ====
asXML:
asJSON: {}
== 02 == $resultJSON ====
{}
== 03 == $resultJSON ====
<aNumber>0.123</aNumber>
<aNumberAsString>0.123</aNumberAsString>
<aString>this is a string</aString>
<a key with spaces>It's possible.</a key with spaces>
<aArray>abc</aArray>
<aObject>
    <foo>that</foo>
    <bar>grill</bar>
</aObject>
<aObjectArray>
    <aObject>
        <foo>objArray_01_foo</foo>
        <bar>objArray_01_bar</bar>
        <aArray>five_01_afive_01_bfive_01_c</aArray>
    </aObject>
    <aObject>
        <foo>objArray_02_foo</foo>
        <bar>objArray_02_bar</bar>
        <aArray>five_02_afive_02_bfive_02_c</aArray>
    </aObject>
</aObjectArray>

== 03 == $resultJSON == final ==
{
    "aNumber": 0.123,
    "aNumberAsString": "0.123",
    "aString": "this is a string",
    "a key with spaces": "It's possible.",
    "aArray": [
        "a",
        "b",
        "c"
    ],
    "aObject": {
        "foo": "that",
        "bar": "grill"
    },
    "aObjectArray": [
        {
            "foo": "objArray_01_foo",
            "bar": "objArray_01_bar",
            "aArray": [
                "five_01_a",
                "five_01_b",
                "five_01_c"
            ]
        },
        {
            "foo": "objArray_02_foo",
            "bar": "objArray_02_bar",
            "aArray": [
                "five_02_a",
                "five_02_b",
                "five_02_c"
            ]
        }
    ]
}

Summary

  • enable creating a node as a number
  • createNodeCmd:
    • define nodes with name and type
    • create your JSON like it should look like at the end

Discussion

... your comments

rattleCAD 2022-07-03 ... you may improve this by replace:

dom createNodeCmd -jsonType NONE    elementNode aNumber
dom createNodeCmd -jsonType NONE    elementNode aNumberAsString
dom createNodeCmd -jsonType NONE    elementNode aString
dom createNodeCmd -jsonType NONE    elementNode "a key with spaces"
dom createNodeCmd -jsonType NONE    elementNode foo
dom createNodeCmd -jsonType NONE    elementNode bar
dom createNodeCmd -jsonType NONE    elementNode array
dom createNodeCmd -jsonType ARRAY   elementNode "aArray"
dom createNodeCmd -jsonType OBJECT  elementNode "aObject"
dom createNodeCmd -jsonType ARRAY   elementNode "aObjectArray"
dom createNodeCmd -jsonType NUMBER  textNode    jsonNumber
dom createNodeCmd -jsonType STRING  textNode    jsonString    

by

foreach {nodeName nodeType jsonType} {
    aNumber                 elementNode     NONE
    aNumberAsString         elementNode     NONE    
    aString                 elementNode     NONE    
    "a key with spaces"     elementNode     NONE    
    foo                     elementNode     NONE    
    bar                     elementNode     NONE    
    array                   elementNode     NONE    
    "aArray"                elementNode     ARRAY   
    "aObject"               elementNode     OBJECT  
    "aObjectArray"          elementNode     ARRAY
    jsonNumber              textNode        NUMBER
    jsonString              textNode        STRING
} {
    # dom createNodeCmd -jsonType STRING  textNode    jsonString    
    # puts "      -> dom createNodeCmd -jsonType  [format {%-8s %-12s %s} $jsonType $nodeType $nodeName]"
    dom createNodeCmd -jsonType $jsonType $nodeType $nodeName
} 

sbron 2022-08-18: I wasn't very satisfied with the need to hardcode the createNodeCmd commands. So I came up with a more general approach, using the namespace unknown functionality. While making that, it occurred to me that this also allowed for a nice way to pass in variables to be substituted:

package require tdom 0.9.3-

namespace eval json {
    namespace ensemble create -subcommands build
    namespace eval eval {
        namespace unknown [namespace parent]::unknown
        dom createNodeCmd -jsonType OBJECT elementNode object
        dom createNodeCmd -jsonType ARRAY elementNode array
        dom createNodeCmd -jsonType NUMBER textNode number
        dom createNodeCmd -jsonType STRING textNode string
        dom createNodeCmd -jsonType TRUE textNode true
        dom createNodeCmd -jsonType FALSE textNode false
        dom createNodeCmd -jsonType NULL textNode null

        proc boolean {arg} {
            if {$arg} {
                tailcall true true
            } else {
                tailcall false false
            }
        }
    }

    proc unknown {cmd args} {
        if {[regexp {^([a-z]+)::(.*)} $cmd -> type name]} {
            if {$type in {object array}} {
                # Create an appropriate elementNode
                namespace eval eval [list tdom::fsnewNode \
                  -jsonType [string toupper $type] $name {*}$args]
            } else {
                # Create an appropriate elementNode
                namespace eval eval [list tdom::fsnewNode \
                  -jsonType NONE $name [linsert $args 0 $type]]
            }
        }
    }

    proc build {type script {data {}}} {
        dom createDocumentNode json
        # Make the data dict available as variables in the eval namespace
        namespace eval eval [list variable {*}$data]
        try {
            namespace eval eval \
              [list [namespace which $json] appendFromScript $script]
        } finally {
            # Clean up the variables from the data dict
            namespace eval eval [list unset {*}[dict keys $data]]
        }
        $json jsonType [string toupper $type]
        $json asJSON -indent 2
    }
}

# Demo

set script {
    number::aNumber $number
    string::aNumberAsString $number
    string::aString $str
    boolean::aBool $bool
    "string::a key with spaces" "It's possible."
    array::aArray {
        string $chra
        string $chrb
        string $chrc
    }
    object::aObject {
        string::foo $foo
        string::bar $bar
    }
    array::aObjectArray {
        object {
            string::foo objArray_01_foo
            string::bar objArray_01_bar
            array::aArray {
                string five_01_a
                string five_01_b
                string five_01_c
            }
        }
        object {
            string::foo objArray_02_foo
            string::bar objArray_02_bar
            array::aArray {
                string five_02_a
                string five_02_b
                string five_02_c
            }
        }
    }
}

set data {
    number 0.123
    str "this is a string"
    bool yes
    chra a
    chrb b
    chrc c
    foo that
    bar grill
}

puts [json build object $script $data]