Repository navigation
Move the editor onto ktsu.UndoRedo 3, stop saves and loads from silently losing a schema, and refuse unreadable or unsizable input - #297
Merged
Conversation
ktsu.ImGui.NodeEditor 3.75 depends on ktsu.UndoRedo 3.4, which absorbed ktsu.UndoRedo.Core. Referencing both put every undo type in the editor's compilation twice (CS0433), which is why the NodeEditor bump has been red. The namespaces and API are unchanged, so swapping the reference is enough. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B1AYPNe9rDK8FtuQgiKTnT
…t save TrySave wrote over the target with File.WriteAllText, which truncates before writing, so a full disk or a dropped share left the only saved copy empty or partial. The content now goes to a temporary file beside the target and is swapped in with File.Replace (or File.Move when there is nothing to replace); the temporary file is removed on any failure. Fixes #292. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B1AYPNe9rDK8FtuQgiKTnT
A PascalCase or legacy file loaded as an empty schema and was reported as a successful migration from format version 0, so validation passed and saving from the editor replaced the file with nothing. Load now refuses any root property the serializer's own contract does not name, and says so when the only problem is casing. Fixes #278 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B1AYPNe9rDK8FtuQgiKTnT
{"existingTypes": null}, or a null entry in it, reached IsSpelled and
threw a NullReferenceException with nothing pointing at the file. The
options now respect nullable annotations, so a null where a value is
needed is refused by the deserializer, and a null existing-type entry
is refused by name.
Fixes #291
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B1AYPNe9rDK8FtuQgiKTnT
A "TypeName" or "DefaultKind" written after the object's other properties threw NotSupportedException out of SchemaSerializer.Load, which promises a result. The serializer now accepts metadata in any position, and a type with no discriminator it recognises is reported as InvalidJson, so the CLI says "Could not open" instead of crashing. Fixes #287 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B1AYPNe9rDK8FtuQgiKTnT
…s path A null in a hand-written file reached the model as a null its non-nullable properties never expect, so Load or Validate threw. Load now drops null properties before deserializing, so they take the same default an omitted property does, and reports a null list element as InvalidJson naming it. Fixes #242 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B1AYPNe9rDK8FtuQgiKTnT
A class that contains itself directly, or through a chain of other classes, an Optional or a Result, has no finite size and cannot be generated in any target. Validation now reports one error per cycle, naming the route, and points at an Array or a Handle as the way to write a recursive structure. Fixes #293 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B1AYPNe9rDK8FtuQgiKTnT
|
This was referenced Oct 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Requested by Matt · project thread
This PR carries seven changes, one commit each.
ImGui 3.75 / UndoRedo 3 (supersedes #277)
Before: the editor stays on ktsu.ImGui.NodeEditor 3.38.5. Dependabot's bump (#277) fails with CS0433. NodeEditor 3.75 brings in ktsu.UndoRedo 3.4, which absorbed ktsu.UndoRedo.Core, so every undo type reached the editor's compilation twice.
After: NodeEditor is at 3.75.3, and the editor references ktsu.UndoRedo 3.4.0 in place of ktsu.UndoRedo.Core 1.1.4.
How: the namespaces and API did not change, so this is a package swap in
Directory.Packages.propsand the editor's csproj, with no code edits.A failed save keeps the last save (fixes #292)
Before:
SchemaFile.TrySavewrote over the target withFile.WriteAllText, which truncates the file before writing. A write that failed partway, such as on a full disk or a dropped network share, left the only saved copy empty or partial, and the editor's memory held the only intact copy.After: the target holds either the complete previous save or the complete new one, and a failed save leaves nothing behind in the folder.
How: the JSON is written to
<name>.tmp-<guid>beside the target. It is then swapped in withFile.Replace, or withFile.Movewhen there is no existing file to replace. The temporary file is deleted on any failure. An internal overload takes the write step as a delegate, so three new tests can make it fail partway and check that the original bytes are unchanged and no temporary file remains. The optionalSchemaGenerator.GenerateToDiskchange is left out, because generated output can be regenerated.A file the model cannot read is refused, not loaded empty (fixes #278)
Before: a PascalCase or legacy file (
{"Classes": [...]}) loaded as an empty schema and reported "Migrated from format version 0". Validation passed, and saving from the editor replaced the file with nothing.After:
SchemaSerializer.LoadreturnsInvalidJsonfor any root property the model does not read. When the only problem is casing, the message names the camelCase spelling, for example'Classes'would be'classes'. A file from a newer format version is still reported as newer, because the version check runs first.How: the known names come from the serializer's own
JsonTypeInfoforSchema, so a property added to the model is accepted as soon as it can be read. The new tests check that every committed legacy sample is refused and every modern sample still loads.docs/schema-format.mddocuments the refusal.A null in the C++ options file is refused, not thrown (fixes #291)
Before:
kschema --cpp-optionsthrew aNullReferenceExceptionfor{"existingTypes": null}or{"existingTypes": {"Kilograms": null}}, and the error did not point at the file.After: both are refused with a message naming the key. A null for any other value the options need, such as
generatedByor a spelling'sinclude, is also refused.How:
CppGeneratorOptionsFilesetsRespectNullableAnnotations, so the deserializer refuses nulls in non-nullable properties. Dictionary values fall outside that setting, soIsSpelledchecks for a null entry itself. Six new tests cover these cases.A discriminator out of position loads, and a missing one is refused (fixes #287)
Before: a type written as
{ "quantityName": "Mass", "TypeName": "Quantity" }, or a default withDefaultKindaftervalue, threwNotSupportedExceptionout ofSchemaSerializer.Load. A type with no recognised discriminator ({}or{ "typeName": "Int" }) threw the same exception, sokschema validatecrashed instead of reporting the file.After: the discriminator can appear anywhere in its object. A type without one is reported as
InvalidJson, and the CLI says "Could not open".How:
SchemaSerializer.JsonOptionssetsAllowOutOfOrderMetadataProperties, andLoadalso catchesNotSupportedException, which is how the serializer reports a polymorphic shape it cannot construct. Four new tests cover these cases.A null in a hand-written schema no longer throws (fixes #242)
Before:
"classes": null,"members": null, a null class name or a null member type loaded "successfully" and then threw aNullReferenceExceptionfromValidate(), the editor or a generator. A null list element, such as"classes": [null], did the same.After: a null property reads exactly as if it were omitted, which is what the serializer already writes, so the default (an empty list, an empty name,
None) takes over and validation reports anything that is then incomplete, such as "empty name". A null element in a list is refused asInvalidJson, with its path in the message, for example$.classes[0].members[0]. The CLI says "Could not open".How:
Loadparses the document into aJsonNode, removes null properties and stops at the first null array element, then deserializes from the node. Nothing in the model is a dictionary or extension data, so "null means omitted" holds everywhere. Thirteen new tests cover these cases, anddocs/schema-format.mddocuments the rule.A class that holds itself by value is an error (fixes #293)
Before: a class holding itself, directly (
TreeNode.Parent : TreeNode) or through other classes, anOptionalor aResult, validated cleanly. It has no finite size, so no generator could produce a type that compiles.After: validation reports one error per cycle, at the member that closes it, naming the route (
A.b → B.c → C.a → A) and suggesting anArrayor aHandle, which is how a recursive structure is meant to be written. A reference through anArray,HandleorInterfaceis not a cycle, and neither is a diamond.How: a depth-first walk over the classes each member holds by value, which visits each class once, so a cycle is reported once rather than once per class on it. The existing test for two
travelsAsBytesclasses holding each other now expects this error. Six new tests cover these cases, and disabling the report fails five of them.The build is clean and all 544 tests in
Schema.Testpass on net10.0 locally.🤖 Generated with Claude Code
https://claude.ai/code/session_01B1AYPNe9rDK8FtuQgiKTnT