6

I have a found a plugin that enables Vim to parse JSON. I need to export VimScript dictionaries as JSON. Currently I am just using:

let str = string(dict)
substitute(str, "'", '"', 'g')

This is working but is bound to break when I run into dictionaries with embedded quotes. What is a better way?

dreftymac
  • 31,404
  • 26
  • 119
  • 182
Sean Mackesey
  • 10,701
  • 11
  • 40
  • 66

5 Answers5

7

Two and a half years after my original answer, there is a simpler alternative: upgrade to Vim 8 and use json_encode() or (if you do not need strict JSON) js_encode().

benjifisher
  • 5,054
  • 16
  • 18
4

I am not sure whether this should be a separate answer, an edit to @Kent's answer, or a comment on @Kent's answer. Here is a version of @Kent's function with a few simplifications:

function! ToJson(input)
    let json = ''
    if type(a:input) == type({})
        let json .= "{"
        let di =  0
        for key in keys(a:input)
            let di += 1
            let json .= '"'.escape(key, '"').'":'
            let json .= ToJson(a:input[key])
            let json .= di<len(a:input)? "," : ""
        endfor
        let json .= "}"
    elseif type(a:input) == type([])
        let json .= "["
        let li = 0
        for e in a:input
            let li += 1
            let json .= ToJson(e)
            if li<len(a:input)
                let json .= ","
            endif
        endfor
        let json .=  "]"

    else
        let json .= '"'.escape(a:input, '"').'"'
    endif
    return json
endfunction

Instead of echoing the result, this returns it as a String. Also, I use escape() instead of substitute(). Finally, I think (Check this!) that it is safe to use escape() as I did without first checking that the argument is a String.

Here is a shorter version, using map(). I do not know whether depth of recursion matters more for this version or the others, but if your input is big enough for that to matter, it probably goes faster if we let map() handle the recursion.

function! ToJson(input)
    let json = ''
    if type(a:input) == type({})
        let parts = copy(a:input)
        call map(parts, '"\"" . escape(v:key, "\"") . "\":" . ToJson(v:val)')
        let json .= "{" . join(values(parts), ",") . "}"
    elseif type(a:input) == type([])
        let parts = map(copy(a:input), 'ToJson(v:val)')
        let json .= "[" . join(parts, ",") . "]"
    else
        let json .= '"'.escape(a:input, '"').'"'
    endif
    return json
endfunction

Using either version, I get the same result as @Kent's function, except for whitespace. I have not tested it with anything more complex than @Kent's d1. It might be safer to use deepcopy() than copy().

benjifisher
  • 5,054
  • 16
  • 18
  • +1 your answer. I was trying using `escape()` too, however I might make some stupid mistake, in output there is no `slash`. and I didn't test further and used `sub..()`. also for the map, I thought of it too, but I need in each recursion step `deepcopy` the object, and I thought it **could** be some problem if the dict has relative more nesting levels. But I didn't give it deep thought. – Kent Apr 28 '14 at 08:41
4

Since Vim 7.4.1304 (so definitely available with Vim 8.0), these functions are built into Vim as json_encode() and json_decode().

For backwards compatibility with even older Vim versions, the WebAPI.vim plugin has a JSON parser and encoder implemented in pure Vimscript:

:let jsonString = webapi#json#encode({...})
Ingo Karkat
  • 167,457
  • 16
  • 250
  • 324
2

I wrote a little function to echo the json string, with " double quote escaped. not sure if it works for your needs:

function! ToJson(input)
    if type(a:input) == type({})
        echo "{"
        let di =  0
        for key in keys(a:input)
            let di += 1
            if type(key) == type('')
                echo '"'.substitute(key, '"', '\\"','g').'":'
            else
                echo '"'.key.'":'
            endif
            call ToJson(a:input[key])
            echo di<len(a:input)? "," : ""
        endfor
        echo "}"
    elseif type(a:input) == type([])
        echo "["
        let li = 0
        for e in a:input
            let li += 1
            call ToJson(e)
            if li<len(a:input)
                echo ","
            endif
        endfor
        echo  "]"

    elseif type(a:input) == type('')
        echo '"'.substitute(a:input, '"', '\\"','g').'"'
    else
        echo '"'.a:input.'"'
    endif
endfunction

a dict like:

let d1={'one':'"""', 'two':123, 333:['11',22,'"_"_"'], 'four':"''"}

will be output as:

{
"four":
"''"
,
"one":
"\"\"\""
,
"two":
"123"
,
"333":
[
"11"
,
"22"
,
"\"_\"_\""
]
}

I didn't do much debug/testing. also the format looks not nice, but I guess you don't care about the format since you used string()...

the above output could be formatted into (by an online json formatter):

{
   "four":"''",
   "one":"\"\"\"",
   "two":"123",
   "333":[
      "11",
      "22",
      "\"_\"_\""
   ]
}

hope it helps.

Kent
  • 189,393
  • 32
  • 233
  • 301
0

Based on @benjifisher answer, I evolved ToJson.

Indentation is hacky, but mostly works.

" Inspect variables
"
" input: variable
" level: actual level of nest
" max: maximum level of nest      let json = ''    
function! ToJson(input, level, max)

  if a:level < a:max
    if type(a:input) == type({})
      let parts = copy(a:input)
      call map(parts, '"\"" . escape(v:key, "\"") . "\":" . ToJson(v:val, ' . (a:level+1) . ',' . a:max . ")")
      let space = repeat(" ", a:level)
      let json .= space . " {\r\n " . space . join(values(parts), ",\r\n " . space) . "\r\n" . space ." }"
    elseif type(a:input) == type([])
      let parts = map(copy(a:input), 'ToJson(v:val,' . (a:level+1) . ',' . a:max . ')')
      let json .= "[" . join(parts, ",\r\n") . "]\r\n"
    elseif type(a:input) == type(function("tr"))
      let dictFunc = substitute(string(a:input), "function('\\(.\\+\\)')", "\\1", "")
      if dictFunc+0 > 0
        let funcName = '{' . dictFunc . '}'
      else
        let funcName = a:input
      endif
      let json .= '"'.escape(genutils#ExtractFuncListing(funcName, 0, 0), '"') . "\""
    else
      let json .= '"'.escape(a:input, '"') . "\""
    endif
  else
    try
      let json .= '"' . escape(string(a:input), '"') . "\""
    catch
      "string() can throw an E724 (too much nest)
      let json .= '"' . escape(keys(a:input), '"') . "\""
    endtry
  endif
  return json
endfunction

To show function definitions you need a dependency from https://github.com/vim-scripts/genutils:

https://github.com/vim-scripts/genutils/blob/master/autoload/genutils.vim#L57

Omit that part if it does not apply to you, is supposed to be used on a visual vim debugger vim-breakpts to inspect complex variables

albfan
  • 12,542
  • 4
  • 61
  • 80