Making pretty JSON

In the new framework there is function that will convert a Dictionary or Auto() to JSON, and it’s preferable to the old JSONItem because 1) it’s easier to use and 2) it generates better JSON. The only deficiency is that you cannot generate “pretty” JSON.

One solution is to covert the JSON text to a JSONItem, set Compact to False and generate JSON again but I don’t particularly care for that since the old JSONItem suffers from some bugs that will never be fixed because of the replacement. Further, once we have the text we need, it only needs to be formatted, not generated again.

With that goal, I came up with this function. It uses a RegEx (not available in iOS — yet) to insert EOL’s at the appropriate places, then pads the lines appropriately.

I’ve only done some basic testing so this is subject to change.

First the pattern, stored in a constant:

(?x)

(?(DEFINE)
	(?<ob>[[{])
	(?<cb>[\\]}])
	(?<string>"(?:\\\"|[^"])*")
	(?<key>(?&string))
	(?<val>(?&string)|[^,}\\]]+)
)

\\[\\],? | \\{\\},? |
(?&ob) | 
(?&key) : (?&ob) |
(?&key) : (?&val) ,? |
(?&val) ,? |
(?&cb) ,?

Now the function:

Function ToJSON_MTC(Extends dict As Xojo.Core.Dictionary, pretty As Boolean = False) As Text
  const kEOL = &u0A
  
  dim json as Text = Xojo.Data.GenerateJSON( dict )
  
  #if not TargetiOS
    
    if pretty then
      static rx as RegEx
      if rx is nil then
        rx = new RegEx
        rx.SearchPattern = kPrettyJSONRegEx
        rx.ReplacementPattern = "$&" + kEOL
        rx.Options.ReplaceAllMatches = true
      end if
      
      dim jString as string = rx.Replace( json )
      
      dim lines() as string = jString.Split( kEOL )
      dim indent as string = ""
      dim pad as string = "  "
      for i as integer = 0 to lines.Ubound
        dim thisLine as string = lines( i )
        dim nextIndent as string = indent
        
        dim firstChar as string = thisLine.Left( 1 )
        dim lastChar as string = thisLine.Right( 1 )
        dim firstTwo as string = thisLine.Left( 2 )
        
        if firstTwo = "[]" or firstTwo = "{}" then
          //
          // Do nothing
          //
        elseif firstChar = "{" or firstChar = "[" or lastChar = "{" or lastChar = "[" then
          nextIndent = indent + pad
        elseif firstChar = "}" or firstChar = "]" then
          indent = indent.LeftB( indent.LenB - pad.LenB )
          nextIndent = indent
        end if
        thisLine = indent + thisLine
        lines( i ) = thisLine
        
        indent = nextIndent
      next
      
      jString = join( lines, kEOL )
      json = jString.ToText
    end if
    
  #endif
  
  return json
  
End Function

After more testing, I’ve revised the RegEx and came up with code that works in iOS too. First the pattern:

(?x)

(?(DEFINE)
	(?<ob>[[{])
	(?<cb>[\\]}])
	(?<string>"(?:\\\"|[^"])*")
	(?<key>(?&string))
	(?<val>(?&string)|[^,}\\]]+)
)

\\[\\],? | \\{\\},? |
(?&ob) | 
(?&key) : (?:\\{\\} | \\[\\]) ,? |
(?&key) : (?&ob) |
(?&key) : (?&val) ,? |
(?&val) ,? |
(?&cb) ,?

Here is the main code, called from Extends on both an Auto() and Xojo.Core.Dictionary:

Private Function ToJSON(o As Auto, pretty As Boolean = False) As Text
  dim json as text = Xojo.Data.GenerateJSON( o )
  if pretty then
    #if TargetiOS
      json = PrettyJSONiOS( json )
    #else
      json = PrettyJSON( json )
    #endif
  end if
  
  return json
End Function

And the pretty-making code:

Private Function PrettyJSON(json As Text) As Text
  #if TargetWin32
    const kEOL as string = &u0D + &u0A
  #else
    const kEOL as string = &u0A
  #endif
    
  static rx as RegEx
  if rx is nil then
    rx = new RegEx
    rx.SearchPattern = kPrettyJSONRegEx
    rx.ReplacementPattern = "$&" + kEOL
    rx.Options.ReplaceAllMatches = true
  end if
  
  dim jString as string = rx.Replace( json )
  
  dim lines() as string = jString.Split( kEOL )
  dim newLines() as string
  
  const kPad as string = "  "
  dim padLenB as integer = kPad.LenB
  
  dim indent as string = ""
  
  for i as integer = 0 to lines.Ubound
    dim thisLine as string = lines( i )
    dim nextIndent as string = indent
    
    dim firstChar as string = thisLine.Left( 1 )
    dim lastChar as string = thisLine.Right( 1 )
    dim firstTwo as string = thisLine.Left( 2 )
    
    if firstTwo = "[]" or firstTwo = "{}" then
      //
      // Do nothing
      //
    elseif firstChar = "{" or firstChar = "[" or lastChar = "{" or lastChar = "[" then
      nextIndent = indent + kPad
    elseif firstChar = "}" or firstChar = "]" then
      indent = indent.LeftB( indent.LenB - padLenB )
      nextIndent = indent
    end if
    
    newLines.Append indent
    newLines.Append thisLine
    newLines.Append kEOL
    
    indent = nextIndent
  next
  
  jString = join( newLines, "" )
  json = jString.ToText
  
  return json.TrimRight
  
End Function

Private Function PrettyJSONiOS(json As Text) As Text
  const kEOL as text = &u0A
  
  dim chars() as text
  for each char as text in json.Characters
    chars.Append char
  next
  
  dim newChars() as text
  
  dim indent as text
  dim kPad as text = "  "
  dim padLen as integer = kPad.Length
  
  dim inString as boolean
  dim charIndex as integer = 0
  while charIndex <= chars.Ubound
    dim char as text = chars( charIndex )
    dim nextChar as text = if( charIndex = chars.Ubound, "", chars( charIndex + 1 ) )
    
    if char = """" then
      newChars.Append char
      inString = not inString
      
    elseif inString and char = "\" then
      newChars.Append char
      newChars.Append nextChar
      charIndex = charIndex + 1
      
    elseif inString then
      newChars.Append char
      
    elseif char = "{" and nextChar = "}" then
      newChars.Append "{}"
      charIndex = charIndex + 1
      
    elseif char = "[" and nextChar = "]" then
      newChars.Append "[]"
      charIndex = charIndex + 1
      
    elseif char = "," then
      newChars.Append char
      newChars.Append kEOL
      newChars.Append indent
      
    elseif char = "[" or char = "{" then
      newChars.Append char
      newChars.Append kEOL
      indent = indent + kPad
      newChars.Append indent
      
    elseif char = "]" or char = "}" then
      newChars.Append kEOL
      
      indent = indent.Left( indent.Length - padLen )
      newChars.Append indent
      newChars.Append char
      
    else
      newChars.Append char
    end if
    
    charIndex = charIndex + 1
  wend
  
  json = Text.Join( newChars, "" )
  return json
End Function

I’ve compared the output from a fairly complex JSON using each version to JSONItem.ToString and they are identical.

I also ported some C# code for formatting JSON to Xojo a couple months ago:

Format JSON