0

I apologize for asking this question in this manner, but I evidently don't have enough "Reputation" to post a question as a comment in the originating thread.

Someone here made a nice little class to properly indent a JSON string so it's more human-friendly. It works great, except that I'd like to modify it so that empty arrays are represented by "[]" rather than having a bunch of whitespace between the characters (newline, likely several indent characters, etc.). It seemed like such a simple plan.

The original code looks like this:

    private const string INDENT_STRING = "    ";
    public static string FormatJson(string str)
    {
        var indent = 0;
        var quoted = false;
        var sb = new StringBuilder();
        for (var i = 0; i < str.Length; i++)
        {
                var ch = str[i];
                switch (ch)
                {
                    case '{':
                    case '[':
                        sb.Append(ch);
                        if (!quoted)
                        {
                            sb.AppendLine();
                            Enumerable.Range(0, ++indent).ForEach(item => sb.Append(INDENT_STRING));
                        }
                        break;
                    case '}':
                    case ']':
                        if (!quoted)
                        {
                            sb.AppendLine();
                            Enumerable.Range(0, --indent).ForEach(item => sb.Append(INDENT_STRING));
                        }
                        sb.Append(ch);
                        break;
                    case '"':
                        sb.Append(ch);
                        bool escaped = false;
                        var index = i;
                        while (index > 0 && str[--index] == '\\')
                            escaped = !escaped;
                        if (!escaped)
                            quoted = !quoted;
                        break;
                    case ',':
                        sb.Append(ch);
                        if (!quoted)
                        {
                            sb.AppendLine();
                            Enumerable.Range(0, indent).ForEach(item => sb.Append(INDENT_STRING));
                        }
                        break;
                    case ':':
                        sb.Append(ch);
                        if (!quoted)
                            sb.Append(" ");
                        break;
                    default:
                        sb.Append(ch);
                        break;
                }
        }
        return sb.ToString();
    }

I tried modifying it so it had this in it:

                    case '[':
                        sb.Append(ch);
                        if (!quoted)
                        {
                            if (str[i + 1] != ']')
                            {
                                sb.AppendLine();
                                Enumerable.Range(0, ++indent).ForEach(item => sb.Append(INDENT_STRING));
                            }
                        }
                        break;

It blows up on me, complaining that I'm referencing an index that's too large (actually, it complains about something that has a number of possibilities, 'ArgumentOutOfRangeException' being the most likely). I tried adding a tracker to see if i+1 > str.Length, but it still blows up. And the spot in the string it's blowing up at isn't anywhere near a [ or a ]. Indeed, ch is a '{' and str[i+1] is a ','.

Am I making any sense?

I thought about just taking the resulting string and using Regex to rip out any whitespace between [ and ], but that seemed inelegant.

Does anyone have a recommendation for how to modify this otherwise-excellent code to be the way I want? I tried, really I did...

Community
  • 1
  • 1
J.D. Ray
  • 697
  • 1
  • 8
  • 22

1 Answers1

0

I have used a different approach. When i encounter a square bracket i check where is the pairing square bracket, and verify it's not an empty string in between. The expression is this:

isEmptyArray = nextMatchingBracketIndex != -1 ? stringRemainder.Substring(1, nextMatchingBracketIndex - 1).Trim().Length == 0 : false;

The full working code is:

class JsonHelper
    {
        private const string INDENT_STRING = "    ";
        public static string FormatJson(string str)
        {
            var indent = 0;
            var quoted = false;
            var isEmptyArray = false;
            var sb = new StringBuilder();
            char ch = default(char);

            for (var i = 0; i < str.Length; i++)
            {
                ch = str[i];

                switch (ch)
                {
                    case '{':
                        sb.Append(ch);
                        if (!quoted)
                        {
                            sb.AppendLine();
                            Enumerable.Range(0, ++indent).ForEach(item => sb.Append(INDENT_STRING));
                        }
                        break;
                    case '[':
                        sb.Append(ch);

                        var stringRemainder = str.Substring(i);
                        var nextMatchingBracketIndex = stringRemainder.IndexOf(']');

                        isEmptyArray = nextMatchingBracketIndex != -1 ? stringRemainder.Substring(1, nextMatchingBracketIndex - 1).Trim().Length == 0 : false;

                        if (!quoted && !isEmptyArray)
                        {
                            sb.AppendLine();
                            Enumerable.Range(0, ++indent).ForEach(item => sb.Append(INDENT_STRING));
                        }
                        break;
                    case '}':
                        if (!quoted)
                        {
                            sb.AppendLine();
                            Enumerable.Range(0, --indent).ForEach(item => sb.Append(INDENT_STRING));
                        }
                        sb.Append(ch);
                        break;
                    case ']':
                        if (!quoted && !isEmptyArray)
                        {
                            sb.AppendLine();
                            Enumerable.Range(0, --indent).ForEach(item => sb.Append(INDENT_STRING));
                        }
                        sb.Append(ch);
                        isEmptyArray = false;
                        break;
                    case '"':
                        sb.Append(ch);
                        bool escaped = false;
                        var index = i;
                        while (index > 0 && str[--index] == '\\')
                            escaped = !escaped;
                        if (!escaped)
                            quoted = !quoted;
                        break;
                    case ',':
                        sb.Append(ch);
                        if (!quoted)
                        {
                            sb.AppendLine();
                            Enumerable.Range(0, indent).ForEach(item => sb.Append(INDENT_STRING));
                        }
                        break;
                    case ':':
                        sb.Append(ch);
                        if (!quoted)
                            sb.Append(" ");
                        break;
                    default:
                        if (!isEmptyArray)
                            sb.Append(ch);
                        break;
                }
            }
            return sb.ToString();
        }
    }

Here are the test that i verified to work as expected:

  1. Empty types array:

     "types":[]
    
  2. Empty types array with characters in between:

     "types":[       ]
    
  3. Full types array with empty characters at the beginning

     "types":[ "locality", "political"]
    

note: in 1,2,3 i refer to a substring in the original JSON file provided in your link:

{"status":"OK", "results":[ {"types":[ "locality", "political"], "formatted_address":"New York, NY, USA", "address_components":[ {"long_name":"New York", "short_name":"New York", "types":[ "locality", "political"]}, {"long_name":"New York", "short_name":"New York", "types":[ "administrative_area_level_2", "political"]}, {"long_name":"New York", "short_name":"NY", "types":[ "administrative_area_level_1", "political"]}, {"long_name":"United States", "short_name":"US", "types":[ "country", "political"]}], "geometry":{"location":{"lat":40.7143528, "lng":-74.0059731}, "location_type":"APPROXIMATE", "viewport":{"southwest":{"lat":40.5788964, "lng":-74.2620919}, "northeast":{"lat":40.8495342, "lng":-73.7498543}}, "bounds":{"southwest":{"lat":40.4773990, "lng":-74.2590900}, "northeast":{"lat":40.9175770, "lng":-73.7002720}}}}]} 

I have tested the output i got in different tests with JSONLint and it was valid.

It's 1 a.m. my time and I am pretty sure there are some edge case i haven't covered. But this code should give you an alternative approach that works better than what you have right now.

Faris Zacina
  • 14,056
  • 7
  • 62
  • 75