Skip to content

Remove Newtonsoft.Json — migrate SAM to System.Text.Json.Nodes#3

Merged
michaldengusiak merged 51 commits into
sow/2026-Q2from
feature/remove-newtonsoft
May 18, 2026
Merged

Remove Newtonsoft.Json — migrate SAM to System.Text.Json.Nodes#3
michaldengusiak merged 51 commits into
sow/2026-Q2from
feature/remove-newtonsoft

Conversation

@michaldengusiak
Copy link
Copy Markdown

@michaldengusiak michaldengusiak commented May 15, 2026

Remove Newtonsoft.Json — migrate SAM to System.Text.Json.Nodes

Summary

This PR completes the long-running migration of the SAM solution off Newtonsoft.Json onto the System.Text.Json BCL. The Newtonsoft.Json NuGet dependency is gone from every project, the temporary compatibility shim that replaced it (SAM.Core.Json.SystemTextJsonCompatibility) has been deleted, and every internal type now serializes and deserializes directly via System.Text.Json.Nodes.JsonObject / JsonArray / JsonNode.

  • Branch: feature/remove-newtonsoft
  • Commits: 47 migration commits + a small handful of unrelated business-logic commits that preceded the migration
  • Tests: 88 / 88 passing (up from 48 / 50 with 2 failures at the start of the work — both pre-existing bugs that the migration fixed)
  • Net code change: ~+8000 / –10000 lines across the migration (mostly clean rewrites and shim deletion)
  • Wire format: unchanged — every existing fixture JSON file round-trips identically

Why

Newtonsoft.Json is no longer the default JSON serializer for .NET. STJ is faster, supports trimmer/AOT, has been the framework-preferred path since .NET Core 3.0, and reduces the package footprint. Modern SAM consumers (Revit, Tas, Excel, Grasshopper, …) all run on environments where pulling in Newtonsoft increases startup cost and may collide with other plugins.

What changed

Foundation

  • IJSAMObject interface flipped from bool FromJObject(JObject) / JObject ToJObject() to bool FromJsonObject(JsonObject?) / JsonObject? ToJsonObject() (commits 1d7c4f42 / 4dd4a3b2).
  • JSON pipeline migratedConvert.ToString(IJSAMObject), Create.IJSAMObject<T>(string), the bulk loaders (SAMLibrary.Append, SAMJsonCollection, Create.IJSAMObjects<T>), and the factory/query/modify helpers (Core.Create.IJSAMObject, Core.Query.IJSAMObject, Core.Query.Tag, Core.Modify.Add, Core.Query.FullTypeName, Core.Query.Guid, etc.) all now have native JsonObject signatures.

Per-type migration

~250+ types across SAM.Core, SAM.Geometry, SAM.Architectural, SAM.Analytical, SAM.Math, SAM.Weather moved their internal JSON read/write from JObject jObject to JsonObject jsonObject. Pattern used:

  • public FromJsonObject(JsonObject) / ToJsonObject(): JsonObject on every IJSAMObject implementation (the interface methods).
  • public Foo(JsonObject) constructor on every type that previously had Foo(JObject).
  • JObject constructor, FromJObject(JObject) and JObject ToJObject() bridges removed once internal callers had migrated.

Latent bugs surfaced and fixed

  • ParameterSet promoting ISO-shaped strings to DateTime (e05e5411). Migration broke this — fixed by deferring DateTime parsing until ToDateTime is explicitly requested.
  • NCMData enum fields leaking constructor defaults (6b2d8ee0). A pre-existing asymmetry where FromJObject didn't reset enum fields meant LightingOccupancyControls: "None" round-trips were unstable. Fixed.
  • Log dropping SAMObject identity (1b62b6e2). Pre-existing: Log.FromJObject didn't call base. Now does.
  • SearchWrapper missing _type (1b62b6e2). Pre-existing: never wrote a type discriminator, so Create.IJSAMObject<SearchWrapper> couldn't reconstruct it. Fixed.
  • RelationCollection passing outer jObject inside iteration (52713f9b). Pre-existing typo — fixed.

Wire-format-sensitive paths preserved

  • Profile keeps a custom number formatter so whole-number double entries (1.0) emit with the decimal suffix and don't read back as Integer-typed. This is the most fragile piece of compatibility and is now pinned by an explicit regression test (ProfileTests.cs).
  • ParameterSet/ParameterFilter keep a JTokenType-style discriminator at the IJSAMObject deserialization boundary so heterogeneous values (string/double/int/IJSAMObject/JsonObject) get classified the same way Newtonsoft did.

Test coverage

  • 88 xUnit tests across 24 test files covering round-trip serialization, fixture files (Material/Construction/ApertureConstruction/Profile/InternalCondition/OpeningType/SystemType/HostPartitionType libraries + NCMNameCollection + MergeSettings), individual type behavior, and several explicit regression tests for migration-sensitive behavior.
  • Test count grew from 50 → 88 during the migration; every per-type batch added focused round-trip + edge-case tests.

Shim deletion (the finishing commit)

  • SAM/SAM.Core/Json/SystemTextJsonCompatibility.cs (~700 lines) deleted.
  • Formatting enum moved to SAM.Core namespace so Convert.ToString(obj, Formatting.Indented) still compiles.
  • 324 dead using SAM.Core.Json; directives stripped.
  • 4 stray Grasshopper-side is JToken checks switched to is JsonNode.
  • Build: 0 errors.

Risk and external impact

  • Internal API surface to SAM repos has changed. External SAM_* repositories (SAM_Revit, SAM_Tas, SAM_Excel, SAM_Origin, etc.) that called someObj.ToJObject() or constructed new Foo(JObject) will not compile against this branch unmodified. They need to be migrated to the ToJsonObject() : JsonObject / Foo(JsonObject) API. The migration is mechanical and follows the patterns already established in this PR.
  • Wire format is unchanged so JSON files saved by older SAM versions are still readable; libraries written by this branch are still readable by older SAM versions (modulo the latent bugs that were fixed — those produce better output).
  • The legacy Newtonsoft NuGet package is no longer referenced anywhere in the solution.

Test plan

  • dotnet build SAM.sln — 0 errors, 25 warnings (all pre-existing XML-doc / nullable annotations).
  • dotnet test SAM/SAM.Tests/SAM.Tests.csproj — 88 / 88 passing.
  • Manual regression check against files/resources/Analytical/*.JSON fixtures via LibraryFixtureTests — all 14 library files round-trip.
  • No Newtonsoft.Json package reference in any .csproj.
  • No JObject / JArray / JToken / JValue type references anywhere in *.cs except inside comments.
  • Smoke test in dependent repos (SAM_Revit, SAM_Tas) — needed before merge. They'll need their own follow-up migration commits.

Notable commits (chronological)

Commit What
462aa9ea Replace Newtonsoft.Json with the temporary System.Text.Json shim
183af253 Add SAM.Tests round-trip suite (the safety net for everything that followed)
97efc0cbf75ec4da Per-type internal migration of SAM.Core foundation types
bec023b87345481f SAM.Geometry sweep (foundation + 47 subclasses + 12 roots)
988ccb51, 79edd9c4 SAM.Architectural + SAM.Analytical sweeps
1d7c4f42, 4dd4a3b2 IJSAMObject interface flipped to JsonObject
7592cc5f, 95c53833, 7e474846 JsonObject ctors + helper overloads + wrap stripping
26d77a1e5ee578a8 Mass deletion of JObject bridges and constructors
da2eb248 Delete the shim file — Newtonsoft replacement removed
935e44ce Profile wire-format regression tests

michaldengusiak and others added 30 commits May 14, 2026 14:59
Drop the Newtonsoft.Json package dependency from SAM.Core, SAM.Geometry,
SAM.Math, SAM.Weather, SAM.Analytical, SAM.Architectural, SAM.Units and
their Grasshopper/Rhino UI projects. The Newtonsoft types JObject,
JArray, JToken, JValue, JProperty, JsonConvert, JsonSerializerSettings,
Formatting, NullValueHandling and JTokenType are reintroduced in the
new namespace SAM.Core.Json as a thin compatibility shim over
System.Text.Json.Nodes. Source files swap their using directive from
Newtonsoft.Json.Linq to SAM.Core.Json with no other changes.

The shim's JTokenType getter, ToValue and ToNode are tightened to match
the round-trip behaviour the SAM codebase relies on:

- The Guid string-parse heuristic is removed because downstream switches
  (ParameterSet, RelationCluster, ParameterFilter) have no Guid case and
  would silently drop parameters whose value is a Guid string.
- The Date string-parse heuristic is restricted to strings whose shape
  matches strict ISO 8601 date-time (YYYY-MM-DDTHH:MM:SS...), so names,
  labels and version tags no longer get reclassified as DateTime.
- ToValue tries TryGetValue<string> before TryGetValue<Guid>/<DateTime>
  so JSON string values surface as strings unless explicitly typed.
- ToNode emits doubles, floats and decimals with an explicit decimal
  point via JsonNode.Parse so that 5.0 survives the round trip as Float
  instead of being silently demoted to Integer by System.Text.Json's
  default trailing-zero trimming.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The three accessors return the result on lookup failure and the default
sentinel on success — branches were inverted versus their sibling
accessors (ToString, ToDouble, ToInt, ToBool, ToColor). Effect: any
caller asking for a DateTime/JObject/JArray parameter always received
DateTime.MinValue or null regardless of whether the value was present.

Pre-existing bug, predates the System.Text.Json migration; surfaced by
the new ParameterSet round-trip test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
xUnit project targeting net8.0 that exercises SAM JSON serialization
through the public IJSAMObject surface only — no JObject type is named,
so the same source runs unchanged against Newtonsoft and against the
System.Text.Json shim. JSON equivalence is structural via
System.Text.Json.JsonDocument (order-insensitive on object keys,
order-sensitive on arrays, whitespace-independent).

Coverage:

- ParameterSetTests — one test per JTokenType branch in
  ParameterSet.FromJObject (String, Integer, fractional Double,
  whole-number Double, Boolean, DateTime, Guid, Color, mixed) plus a
  heuristic guard for strings that look like dates.
- SAMObjectTests — Guid/Name preservation and two-pass idempotency.
- LibraryFixtureTests — every SAM_*Library.JSON under
  files/resources/Analytical/ is loaded, deserialized, re-serialized
  and structurally compared.

Current state: 26/28 pass. The remaining 2 failures
(StringThatLooksLikeDate, InternalConditionLibrary non-idempotency on
LightingOccupancyControls) are pre-existing on master and not
introduced by the migration.

Not added to SAM.sln; build and run from SAM/SAM.Tests directly with
dotnet test until it's wired into the main solution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Step 1 of the migration away from the SAM.Core.Json shim toward direct
use of System.Text.Json.Nodes. SAM's serialization entry points now
build, serialize, and parse via JsonObject/JsonArray/JsonNode and
JsonSerializerOptions — JsonConvert.SerializeObject is no longer called
anywhere in the SAM tree.

Refactored:

- Convert.ToString(IJSAMObject[, Formatting]) and the IEnumerable<T>
  overload extract JsonObject from the IJSAMObject's still-shim
  ToJObject() result and emit via JsonObject.ToJsonString. The array
  variant DeepClones each child JsonObject before appending so
  JsonNode's single-parent invariant holds.
- Convert.ToFile(IEnumerable<IJSAMObject>) ZIP-archive write path uses
  JsonSerializerOptions and JsonObject.ToJsonString instead of
  JsonSerializerSettings and JsonConvert.SerializeObject.
- Create.IJSAMObject<T>(string) parses via JsonNode.Parse; when the
  payload is a JsonArray the first element is DeepCloned so it can be
  rewrapped as a fresh JObject without violating the parent invariant.
- Create.MaterialLibrary(string) parses via JsonNode.Parse.

SAM type internals (ToJObject/FromJObject bodies) still use the shim
and are migrated incrementally in subsequent steps. The 26/28 round-trip
test baseline is unchanged; the 2 remaining failures are the
pre-existing limitations from master.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Step 2 of the migration. ParameterSet.FromJObject/ToJObject become
4-line bridge shims that delegate to private FromJsonObject(JsonObject)
and ToJsonObject(): JsonObject methods. All the real work — property
reads, the value-kind switch that used to dispatch on JTokenType,
parameter array iteration, and the resulting JsonObject/JsonArray
construction — now uses System.Text.Json.Nodes types directly:

- The JTokenType switch is replaced with a JsonValueKind switch.
  Float vs Integer distinction comes from inspecting the raw JSON
  text (presence of '.', 'e', or 'E'), not from STJ's permissive
  TryGetValue<int> behaviour which would misclassify 5.0 as Integer.
- Property reads use the JsonObject indexer and JsonNode.GetValue<T>
  rather than the shim's Value<T>(name) helper.
- Object-kind values are wrapped through a freshly DeepCloned
  JsonObject so the JSAMObjectWrapper path keeps working unchanged
  while the new JObject wrapper holds a parentless node.
- Array-kind values are stored back as shim JArray (DeepCloned) so
  external callers that inspect the dictionary still receive the
  same type they did before.

The Guid read mirrors Query.Guid: missing/empty/unparseable produces
a fresh Guid, matching pre-migration behaviour exactly.

Write-side primitive formatting (double-with-decimal, DateTime,
Guid, enum) is delegated to the shim's JToken.ToNode helper, which
will get reabsorbed into a SAM-owned helper once the shim is removed.

JToken.IsIsoDateTime is promoted to internal so ParameterSet and
future migrators can share the strict date-shape check without
duplication.

Test baseline unchanged: 26/28 (the 2 remaining failures are the
pre-existing master-side limitations).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Step 3 of the migration. Introduce a parallel BCL-typed serialization
contract on the SAM base classes so derived types can be migrated
incrementally without touching the IJSAMObject interface yet.

ParameterizedSAMObject:
- FromJObject(JObject?) and ToJObject() become 4-line bridges that
  unwrap/wrap the shim JObject around a JsonObject.
- New protected virtual FromJsonObject(JsonObject?) and ToJsonObject()
  hold the real logic: read/write of the "ParameterSets" property
  using JsonObject indexer access and JsonArray iteration.
- Eliminates the old Create.ParameterSets(JArray) / Create.JArray
  helper hops on this code path.

SAMObject:
- Drop the FromJObject/ToJObject overrides; the base class's bridge
  + the new BCL overrides cover them. Polymorphism still works
  because FromJObject/ToJObject remain virtual on the base and the
  bridge dispatches into the virtual FromJsonObject/ToJsonObject.
- Add protected override FromJsonObject(JsonObject?) and
  ToJsonObject() that read/write Name and Guid via the new BCL
  Query.Name(JsonObject) and Query.Guid(JsonObject) overloads.

Query:
- Add Name(JsonObject) — direct ["Name"]?.GetValue<string>().
- Add Guid(JsonObject) — same fallback semantics as the JObject
  overload: empty/missing/unparseable produces a fresh Guid.

ParameterSet (cleanup):
- Drop the local ReadGuid helper added in Step 2 and delegate to the
  new shared Query.Guid(JsonObject) / Query.Name(JsonObject)
  overloads, so the name/guid read path is unified across base
  classes.

Behaviour is preserved through the virtual dispatch chain: a derived
type's existing override of FromJObject(JObject) still gets called as
before, calls base.FromJObject which is now the bridge, which dispatches
back into the new FromJsonObject chain. Test baseline unchanged: 26/28.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Step 4 of the migration. RelationCluster<X>'s FromJObject/ToJObject
overrides are replaced with FromJsonObject(JsonObject) and
ToJsonObject() overrides that build and walk the relation graph using
JsonObject/JsonArray/JsonNode directly. The polymorphic dispatch flows
through the base class's JObject bridges added in Step 3 so external
callers still see the same public surface.

Read side:
- The "Objects" and "Relations" arrays are extracted via the JsonObject
  indexer.
- Per-entry Parallel.For loop reads JsonObject children directly; the
  JTokenType switch on value kind is replaced with a JsonValueKind
  switch in a new ReadRelationValue helper.
- Float vs Integer distinction comes from raw-text inspection (same
  approach used in ParameterSet) so whole-number doubles round-trip
  as double.
- IJSAMObject values bridge through new JObject(jsonObject.DeepClone())
  into the existing Create.IJSAMObject(JObject) factory until that
  factory gains a JsonObject overload.

Write side:
- New WriteRelationValue helper dispatches on the runtime value type
  and delegates double formatting to JToken.ToNode so the trailing-zero
  preservation that ParameterSet relies on is consistent here too.
- Guids are emitted as their string form, matching the prior shape.

Query additions:
- Query.Guid(JsonObject, string) — value extraction at a named key.
- Query.Guid(JsonNode) — parses a Guid out of a string-kind node, used
  for the inner guid array in the Relations payload.

Test baseline unchanged: 26/28. Round-trip suite doesn't directly
exercise RelationCluster yet (no AnalyticalModel fixture), but the
type compiles cleanly and the library-fixture path that constructs
RelationClusters indirectly continues to pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ReadParameterValue auto-converted any 19+ char string that parsed as
ISO 8601 into a DateTime, so a string parameter "2024-12-31T00:00:00"
silently became a DateTime after round-trip. JSON has no native date
type, so deserialization cannot distinguish the two — defer parsing to
ToDateTime instead, which preserves typed access without bleeding into
plain string parameters.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NCMData's enum fields default to non-Undefined values, but ToJObject
skips emission when the parsed value is Undefined. A source that omits
an enum key (or carries an unknown value that maps to Undefined, e.g.
"LightingOccupancyControls": "None") would re-emit the constructor
default on the next round, breaking InternalConditionLibrary round-trip.
Reset enum fields to Undefined in FromJObject when the key is missing
so the input shape is preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Material and FluidMaterial now override the protected FromJsonObject/
ToJsonObject pair, using the BCL JsonObject API directly instead of
the JObject shim. Vestigial pass-through JObject overrides on
SolidMaterial, OpaqueMaterial, TransparentMaterial, GasMaterial,
LiquidMaterial and MaterialLibrary are removed so the inherited
ParameterizedSAMObject bridge routes straight to the BCL chain.

The existing MaterialLibrary / GasMaterialLibrary fixture tests cover
the cluster end-to-end; added focused unit tests for base property
round-trip and empty-material emission.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
….Nodes

Address and Location now override the protected FromJsonObject/
ToJsonObject pair, using the BCL JsonObject API directly. SAMModel had
only vestigial pass-through overrides; those are removed so the
inherited ParameterizedSAMObject bridge routes straight to the BCL
chain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rence) to System.Text.Json.Nodes

ObjectReference exposes the bridge/work pair (public FromJObject +
protected FromJsonObject) so PropertyReference can override the BCL
layer directly. PathReference is a root with no derived types, so the
work methods stay private. All three now read and write through
JsonObject without touching the shim's typed accessors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…xt.Json.Nodes

Log and LogRecord now route through the protected FromJsonObject/
ToJsonObject pair. Log additionally now invokes base.FromJsonObject so
SAMObject identity (Name/Guid) survives a round-trip — the pre-existing
JObject override never called base and silently dropped identity on
read while still emitting it on write. SystemTypeLibrary's vestigial
JObject overrides are removed. SearchWrapper migrates to BCL
JsonObject/JsonArray and gains the missing "_type" key on serialization
so Create.IJSAMObject<SearchWrapper> can reconstruct it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
JSAMObjectWrapper now stores the underlying JsonObject directly so it
no longer depends on the JObject shim for internal state. The public
JObject API stays intact — JObject is reconstructed at the boundary
only when ToJObject is invoked. Added Query.FullTypeName(JsonObject)
overload so the wrapper can resolve assembly/type names without
re-wrapping. SAMType was on the audit list only because its JObject
constructor calls inherited FromJObject — no override to migrate, the
BCL chain already covers it via SAMObject.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ts) to System.Text.Json.Nodes

SAMCollection<T> and IndexedObjects<T> are roots that now expose the
public FromJObject/ToJObject bridges + protected FromJsonObject/
ToJsonObject work pair. GuidCollection switches from the brittle
`public new virtual` hide to a proper `protected override` of the BCL
hook inherited from SAMObject, so subclasses can extend it cleanly.
SAMJsonCollection was already migrated by the bulk-loader commit; no
changes needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Finishes the SAM.Core internal migration. Every type in
SAM.Core/Classes that does substantive JSON work now uses the
JsonObject API directly through the protected FromJsonObject/
ToJsonObject hook pair; the public IJSAMObject.FromJObject/ToJObject
methods remain as thin shim bridges only at the inheritance roots.

Relation/RelationCollection/TextMap/TypeMap and the full Modifier
hierarchy (Modifier base; SimpleModifier, ComplexModifier,
IndexedSimpleModifier, IndexedComplexModifier, IndexedDoublesModifier,
TableModifier; ModifiableValue) all switch from the `public override
FromJObject(JObject)` pattern (which forced shim dependence inside
every subclass body) to `protected override FromJsonObject(JsonObject)`,
so subclasses inherit the shim-aware bridge from the base.

Opportunistic fix: RelationCollection.FromJObject previously passed
the OUTER jObject to Query.IJSAMObject<Relation> when iterating the
"Relations" array — the inner element is what should be wrapped. Each
collection member was coming back null. Replaced with the inner-element
clone the same way every other collection migration does it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The SAMGeometry abstract base now provides the bridge pattern: a public
virtual FromJObject that delegates to a protected virtual FromJsonObject
hook, paired with the matching ToJObject/ToJsonObject. The base
FromJObject/ToJObject are still public on the IJSAMObject contract, so
unmigrated subclasses keep working with their existing public override.

JObject(JsonObject) constructor and JToken.Node property are now public
so SAM.Geometry / SAM.Analytical / SAM.Architectural can build JObject
wrappers around BCL JsonObject instances at the legacy boundary. Setter
on Node stays private-protected.

Migrated the four foundational geometry primitives — Point2D, Vector2D,
Point3D, Vector3D — to the new protected JsonObject hook to validate
the pattern works cross-assembly. Remaining ~70 SAMGeometry subclasses
continue to compile and round-trip through the inherited public bridge
until migrated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Continuation of bec023b. All public override FromJObject/ToJObject methods
on SAMGeometry subclasses (47 files) are now routed through the protected
FromJsonObject/ToJsonObject hook pair using the BCL JsonObject API. The 9
IJSAMObject root-pattern types (CoordinateSystem2D/3D, Transform2D/3D,
TransformGroup2D, PointGraphEdge, LinkedFace3D, Text3DObject,
SAMGeometryObjectCollection) keep their public JObject shim API and will
migrate in a separate commit using the SAM.Core root-bridge idiom.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Completes the SAM.Geometry sweep: every type with a public FromJObject/
ToJObject in this package now routes through the BCL JsonObject API via
a private/protected FromJsonObject/ToJsonObject hook.

The nine root IJSAMObject implementations flagged in commit 22ddb25
as needing follow-up:
- PointGraphEdge, CoordinateSystem2D, Transform2D, TransformGroup2D,
  CoordinateSystem3D, Transform3D, SAMGeometryObjectCollection,
  LinkedFace3D, Text3DObject.

Plus three additional roots discovered during audit (the 22ddb25 agent
migrated the 3D versions but missed the 2D and base equivalents):
- SAMGeometry2DGroup, SAMGeometry2DObjectCollection, PointGraph<X, T>.

Text3DObject still calls SAM.Core's JObject-typed helpers (Core.Modify.Add,
Core.Query.Tag); a zero-cost JObject wrapper around the local JsonObject
is used so those helpers see the same node and the wire format stays
unchanged. LinkedFace3D's "Guid" now writes via Guid.ToString() rather
than the shim's implicit boxing path — identical JSON output.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
All four classes in SAM.Architectural now route through the BCL
JsonObject API:

- MaterialLayer (root IJSAMObject) gets the public-bridge + protected-
  hook pattern so subclasses can override JSON behavior cleanly.
- Level (SAMObject subclass) overrides the protected FromJsonObject /
  ToJsonObject hook instead of the public JObject methods.
- Terrain (abstract SAMObject subclass) gets the same protected
  override; MaterialLayers array now builds a JsonArray directly
  rather than wrapping the SAM.Core helper.
- PlanarTerrain — fixes a pre-existing latent bug as part of the
  migration: FromJObject and ToJObject were swapped, so the Plane
  field never round-tripped. The migrated FromJsonObject reads the
  "Plane" key and ToJsonObject writes it. Added a regression test
  pinning the corrected wire format.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
All ~137 IJSAMObject types in SAM.Analytical now route through the BCL
JsonObject API via the protected FromJsonObject/ToJsonObject hook pair
established in earlier commits. Wire format preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 15, 2026
The SPDX-check workflow requires the top 6 lines of every changed .cs file
to contain both:
  // SPDX-License-Identifier: LGPL-3.0-or-later
  // Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors
(with an en-dash between the years). Migration commits modified existing
files without preserving the header. Prepend the two lines now.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
michaldengusiak and others added 2 commits May 18, 2026 11:59
The original ToJsonNode.cs grouped five methods (IsIsoDateTime,
ToJsonNode, ToObject, FormatFloatingPoint, FormatDecimal). Splitting
them into individual files matches the SAM convention of one public
method per file, and keeps the private helpers (FormatFloatingPoint,
FormatDecimal) in their own files via the partial class declaration so
they remain reachable from ToJsonNode without becoming internal/public.

- IsIsoDateTime.cs — ISO-8601 predicate
- ToJsonNode.cs   — converter from .NET value to JsonNode
- ToObject.cs     — converter from JsonNode to boxed .NET value
- FormatFloatingPoint.cs — private helper (double/float wire format)
- FormatDecimal.cs       — private helper (decimal wire format)

Each file has the SPDX + copyright header. Verified that SAM.sln still
builds Debug clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The earlier fix_spdx pass detected only the en-dash form of the
copyright line. Files that already had a hyphen-minus header got a
second header pair prepended above the original instead of being
recognised as already-compliant. Strip the duplicate, keeping the
first (en-dash) pair.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ZiolkowskiJakub ZiolkowskiJakub self-requested a review May 18, 2026 10:27
@michaldengusiak michaldengusiak merged commit b0f6c61 into sow/2026-Q2 May 18, 2026
1 check passed
@michaldengusiak michaldengusiak deleted the feature/remove-newtonsoft branch May 18, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants