Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

JsonEdit: Quick JsonData Editor-Control

0.00/5 (No votes)
26 Jan 2021 1  
JSchema-Support, easy usage UI, smart line-layout
From a given Json-Schema, the JsonEdit-control creates a formula for input valid Json-Data. Additionally, I introduce a line-break-algorithm, which improves readability of Json-Codes (a bit).

JsonEdit UI-Concept

JsonEdit is designed to show and edit a Newtonsoft JToken-Object.
Prerequisite is to attach a JSchema-Object to the token, which defines the tokens Structure.
See the code to do that:

// (JToken tk, JSchema schema)
tk.AttachSchema(schema);
jEditor1.Token = tk;

Then the Control may display the following:

Image 1

The Root-Node displays [2], which is the Represantation of an Array-Element in the underlying Jsoncode, with two (complex) Elements.
In contrast to that the next Treenode - {4} - represents an Object-Element. With four Properties.
Note: The Root-Node is selected, but the Focus is on the Input-Textbox.
Hmm - in Json editable Data only appears as "Primitive":
Either as Property-Value (eg. Nodes #3-5), or as Array-Element (Nodes #7-9).

Whereas for an Array-Node, the input-Textbox is locked for text-input.

But what you can do (via the locked but focused Textbox), is either navigate with 'Arrow-Up/Down' the Treeview-Selection to an editable Node.
Or you press '+':

Image 2

The latter inserts a new (complex) Element within the JArray, and populates it with defaults, as they are defined in the underlying JSchema.

As said you can navigate (with Mouse or Arrow-Down) to data-entries you don't like, and edit them:

Image 3

Inserting Array-Elements works on any Array, defined in the Schema, and the outcome of generated Elements depends on the "items" - definition, given in the particular SubSchema.
Here I added an Element to the Array-Property, named "powers". Since its Elements are defined as primitive, I can input text:

Image 4

Of course, you also can remove JArray-Elements: Select the Element (not the Array itself!) and press 'Ctrl-Del':

Image 5 . . . Image 6

As least to mention: JsonEdit not only edits given JTokens, but also can create one from scratch (according to a given Schema).

The Magic Behind

I created some wired Extension-Methods, which provide a kind of "Attached Property-Container" to JToken.
Within that "Attachment", I associate the Token with its Schema.

When a (Root-)Token is to display, I recursively traverse Schema and Token in parallel, and to each Sub-Token I attach its Sub-Schema.

(Moreover, I attach a TreeNode to them, to get all things together.)
That is why I can add a proper Array-Element (even a complex one), when the user selects an Array-Treenode and presses '+'.

In this article, I spare you the code of that - see the source if you like.

The Demo - Application

I put some Sample-Data into a typed Dataset and bound some Datagridviews. So one can select prepared JSchemas and for each Schema can choose from several prepared Tokens.

Below, current Schema and Token are displayed as raw Json-Code "as-is".
On the right, the JsonEdit-UserControl performs Data-Input with user-guidance as intuitive it can.

Image 7

Let's walk through all Buttons you can click:

  1. "Schema build Token"
    Builds a new Root-Token, from selected Schema, populated with defaults.
  2. "Edit Token"
    Attaches the selected Schema to the selected Token (as displayed below), and push the Token into the JsonEdit
  3. "Dts read Schema"
    reads the attached Schema from the JsonEdit-edited Token into the current Schema-DataRecord.
    (You can use that to transport a Schema to another Record.)
  4. "Dts read Token"
    reads the JsonEdit-edited Token into the current Token-DataRecord.
  5. "Load Dts"
    clears the Dataset and reloads it from Drive.
  6. "Save Dts"
    saves the Dataset to Drive
  7. "Show Node-Json"
    pops up a Messagebox to display the Sub-Token, attached with the selected Treenode
  8. "Show Node-Schema"
    displays the Sub-Schema, attached with the selected Treenode
  9. "Test"
    I have forgotten, what it does atm.
  10. (Schema) "Commit edit"
    Writes the Schema from the Schema-Textbox into the current Schema-DataRecord.

Note: Error-Messages will appear, when you try to store invalid Json into the Dataset.
Moreover, an ErrorProvider will blink when the Textbox-Input fails validating against the current Sub-Schema.

(Nevertheless, I hacked some invalid JSchema-Samples into the Data, to test Error-Messages of my Line-break-Algorithm.)

Line-break Algorithm

To me common Json-Listings, as provided eg. by Newtonsoft.Json.JToken.ToString() are quite long. So I searched (and found) a way to reduce the amount of lines by my own line-break-algorithm, where I can define a maxLineLength.

See example in comparison.

Long version:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "address": {
      "type": "object",
      "properties": {
        "street_address": {
          "type": "string"
        },
        "city": {
          "type": "string"
        },
        "state": {
          "type": "string"
        }
      },
      "required": [
        "street_address",
        "city",
        "state"
      ]
    }
  },
  "type": "object",
  "properties": {
    "SomeNumbers": {
      "type": "array",
      "items": {
        "type": "number",
        "default": 1
      }
    },
    "Addresses": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/address"
      }
    },
    "ZeitRabat": {
      "type": "integer"
    },
    "complex_element": {
      "type": "object",
      "properties": {
        "street_address": {
          "type": "string",
          "default": "Broadway"
        },
        "city": {
          "type": "string"
        },
        "state": {
          "type": "string"
        },
        "numb": {
          "type": "integer",
          "default": 1
        }
      },
      "required": [
        "street_address",
        "city",
        "state"
      ]
    }
  }
}

Same after applying a linebreak with maxLineLength = 70:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "address": {
      "type": "object",
      "properties": {
        "street_address": {"type": "string"},
        "city": {"type": "string"},
        "state": {"type": "string"}
      },
      "required": ["street_address", "city", "state"]
    }
  },
  "type": "object",
  "properties": {
    "SomeNumbers": {
      "type": "array",
      "items": {"type": "number", "default": 1}
    },
    "Addresses": {
      "type": "array",
      "items": {"$ref": "#/definitions/address"}
    },
    "ZeitRabat": {"type": "integer"},
    "complex_element": {
      "type": "object",
      "properties": {
        "street_address": {
          "type": "string",
          "default": "Broadway"
        },
        "city": {"type": "string"},
        "state": {"type": "string"},
        "numb": {"type": "integer", "default": 1}
      },
      "required": ["street_address", "city", "state"]
    }
  }
}

To me it makes a noticeable difference, whether Json-Code looks like:

"properties": {
  "street_address": {
    "type": "string"
  },
  "city": {
    "type": "string"
  },
  "state": {
    "type": "string"
  }
},

or like:

"properties": {
  "street_address": {"type": "string"},
  "city": {"type": "string"},
  "state": {"type": "string"}
},

Line-break-Code

The algorithm knows nothing about Json. It only looks at the structure of brackets - namely brackets occurring at the end of the line.
Because JToken.ToString() performs a line-break after each opening or closing Bracket.

I show the code because some fancy features may be interesting for some readers:

  • useage of an IEnumerator<String>-Object as kind of "Line-Reader"
  • useage of anonymous methods to outsource cumbersome string-operations from the main-algorithm (you can see that as bit SLA - "Single Level of Abstraction".
  • useage of a (larger) anonymous method, which calls itself recursively.
    Performing recursion by anonymous method is quite handy: you have acces to boundary-conditions, like the maxLineLength, the above mentioned "Line-Reader" and anonymous helper-methods.
    That helps a lot to keep the algorithm together within one method (encapsulation, high local coherence).
public static string[] LineLayout(
      this IEnumerable<string> structuredLines, int maxLineLength) {
   /* recursively identify text-structures like:
    * headLine {
    *   innerLine,
    *   innerLine,
    *   innerLine
    * },
    * (last comma optional)
    *
    * try convert to:
    * headLine {innerLine, innerLine, innerLine},
    *
    * at least ensure propper innerLine-indentation
    */
   var brackets = "{[}]".ToCharArray();
   Func<string, char> lineEndBracket = s => {                // return the bracket
                                    // occurring in the last 2 Chars, otherwise \0
      var i = s.LastIndexOfAny(brackets, s.Length - 1, Math.Min(s.Length, 2));
      return i < 0 ? char.MinValue : s[i];
   };
   Func<char, char> getClosingBracket = openBr =>
                     brackets[Array.IndexOf(brackets, openBr) + 2];
   structuredLines = structuredLines.Where(s => !string.IsNullOrWhiteSpace(s));
   using (var lineEnumerator = structuredLines.GetEnumerator()) {

      Func<string, int, string[]> layoutCore = null; // recursive anonymous function
      layoutCore = (headLine, level) => {
         var counter = headLine.Length + level * 2 - 3;
         var innerLines = new List<string>();
         Action<string> addInnerLine =
                        ln => { innerLines.Add(ln); counter += ln.Length + 1; };
         while (lineEnumerator.MoveNext()) {
            var line = lineEnumerator.Current.Trim();
            var c = lineEndBracket(line);
            if (c == char.MinValue) { addInnerLine(line); continue; }   // no bracket:
                                                         // add innerLine and continue
            if ("}]".Contains(c)) { // close-bracket: return head-,inner-, closing-lines.
                                                                // In 1 line, if possible
               if (c != getClosingBracket(headLine.Last()))
                        throw new InvalidOperationException("[ } - Mismatch");
               // this(!!) ist the point of return!
               return MakeArray(headLine, innerLines, line,
                                 collapse: counter + line.Length < maxLineLength - 1);
            }
            // opening-bracket: call layoutCore() recursively
            var lns = layoutCore(line, level + 1);
            if (lns.Length == 1) addInnerLine(lns[0]);     // if one-liner returned
                                                            // add it to innerLines
            else {    
               // recursion couldn't collapse lines. So this level cannot either
               innerLines.AddRange(lns);
               counter = maxLineLength;             // ensure not to collapse lines
            }
         } // while
           // Exception, because lineEnumerator should exhaust
           // exactly with the last closing-Bracket.
         throw new InvalidOperationException($"Start-Bracket misses counter-part.");
      }; // end recursive anonyme function layoutCore()

      if (!lineEnumerator.MoveNext()) return new string[] { };   //successful return,
                                                               // when empty document
      var rslt = layoutCore(lineEnumerator.Current.Trim(), 0); // recursion-entry-point!!
      if (!lineEnumerator.MoveNext()) return rslt;            // successful return,
                                                // when lineEnumerator is exhausted
      if ("}]".Contains(structuredLines.Last().Last()))
         throw new InvalidOperationException("End-Bracket misses counter-part.");
      throw new InvalidOperationException("lines detected after document-end.");
   } // using
} // LineLayout()

static private string[] MakeArray(
      string headLine, List<string> innerLines, string closingLine, bool collapse) {
   if (collapse) {
      var oneLine = string.Concat(headLine, string.Join(" ", innerLines), closingLine);
      return new string[] { oneLine };
   }
   var indentedInnerlines = innerLines.Select(s => "  " + s);
   return indentedInnerlines.Prepend(headLine).Append(closingLine).ToArray();
} // MakeArray

As far as I see, the code is "over-commented", so for me there is nothing left to explain.

Line-break-Form

To play around with the line-break-algo, the Demo-App comes up with a second Form:

Image 8

You can select the same Schema-Datarecords as on the Main-Form, but they are presented raw versus line-broken
(and some raw-layouts are really messed up).

Conclusion

The JsonEdit-Control is meant as a component in small self-made developper-tools, or for technical-administrators, which may deal with quite raw data.
(Or for playing around. But for playing there are several powerful Web-Tools available in the Internet)

It is propably not bullet-proof, and of course not Json-feature-complete. I don't know all the fancy features, supported by the current JSchema-Standard.
And I'm afraid, my life is too short to figure out all that stuff, and adapt my poor little JsonEdittito to it.

For example: JsonEdit only can handle Properties and Array-"items", when they have one "type" (and optional additional null).
Whereas the JSchema-Specification allows multiple, arbitrary "type"s to any item - if I understand it right.

History

  • 26th January, 2021: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here