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:
tk.AttachSchema(schema);
jEditor1.Token = tk;
Then the Control may display the following:
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 '+'
:
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:
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:
Of course, you also can remove JArray
-Elements: Select the Element (not the Array itself!) and press 'Ctrl-Del'
:
. . .
As least to mention: JsonEdit
not only edits given JToken
s, 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 Datagridview
s. So one can select prepared JSchema
s and for each Schema can choose from several prepared Token
s.
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.
Let's walk through all Button
s you can click:
- "Schema build Token"
Builds a new Root-Token, from selected Schema, populated with defaults. - "Edit Token"
Attaches the selected Schema to the selected Token (as displayed below), and push the Token into the JsonEdit
- "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.) - "Dts read Token"
reads the JsonEdit
-edited Token into the current Token-DataRecord. - "Load Dts"
clears the Dataset
and reloads it from Drive. - "Save Dts"
saves the Dataset
to Drive - "Show Node-Json"
pops up a Messagebox
to display the Sub-Token, attached with the selected Treenode
- "Show Node-Schema"
displays the Sub-Schema, attached with the selected Treenode
- "Test"
I have forgotten, what it does atm. - (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) {
var brackets = "{[}]".ToCharArray();
Func<string, char> lineEndBracket = s => {
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;
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; }
if ("}]".Contains(c)) {
if (c != getClosingBracket(headLine.Last()))
throw new InvalidOperationException("[ } - Mismatch");
return MakeArray(headLine, innerLines, line,
collapse: counter + line.Length < maxLineLength - 1);
}
var lns = layoutCore(line, level + 1);
if (lns.Length == 1) addInnerLine(lns[0]);
else {
innerLines.AddRange(lns);
counter = maxLineLength;
}
}
throw new InvalidOperationException($"Start-Bracket misses counter-part.");
};
if (!lineEnumerator.MoveNext()) return new string[] { };
var rslt = layoutCore(lineEnumerator.Current.Trim(), 0);
if (!lineEnumerator.MoveNext()) return rslt;
if ("}]".Contains(structuredLines.Last().Last()))
throw new InvalidOperationException("End-Bracket misses counter-part.");
throw new InvalidOperationException("lines detected after document-end.");
}
}
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();
}
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
:
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