Remove Newtonsoft.Json — migrate SAM to System.Text.Json.Nodes#3
Merged
Conversation
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>
ZiolkowskiJakub
approved these changes
May 16, 2026
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>
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.
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.JsonNuGet 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 viaSystem.Text.Json.Nodes.JsonObject/JsonArray/JsonNode.feature/remove-newtonsoftWhy
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
IJSAMObjectinterface flipped frombool FromJObject(JObject)/JObject ToJObject()tobool FromJsonObject(JsonObject?)/JsonObject? ToJsonObject()(commits1d7c4f42/4dd4a3b2).Convert.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 jObjecttoJsonObject jsonObject. Pattern used:FromJsonObject(JsonObject)/ToJsonObject(): JsonObjecton every IJSAMObject implementation (the interface methods).Foo(JsonObject)constructor on every type that previously hadFoo(JObject).JObjectconstructor,FromJObject(JObject)andJObject ToJObject()bridges removed once internal callers had migrated.Latent bugs surfaced and fixed
DateTime(e05e5411). Migration broke this — fixed by deferring DateTime parsing untilToDateTimeis explicitly requested.6b2d8ee0). A pre-existing asymmetry whereFromJObjectdidn't reset enum fields meantLightingOccupancyControls: "None"round-trips were unstable. Fixed.1b62b6e2). Pre-existing:Log.FromJObjectdidn't call base. Now does._type(1b62b6e2). Pre-existing: never wrote a type discriminator, soCreate.IJSAMObject<SearchWrapper>couldn't reconstruct it. Fixed.52713f9b). Pre-existing typo — fixed.Wire-format-sensitive paths preserved
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).Test coverage
Shim deletion (the finishing commit)
SAM/SAM.Core/Json/SystemTextJsonCompatibility.cs(~700 lines) deleted.Formattingenum moved toSAM.Corenamespace soConvert.ToString(obj, Formatting.Indented)still compiles.using SAM.Core.Json;directives stripped.is JTokenchecks switched tois JsonNode.Risk and external impact
SAM_Revit,SAM_Tas,SAM_Excel,SAM_Origin, etc.) that calledsomeObj.ToJObject()or constructednew Foo(JObject)will not compile against this branch unmodified. They need to be migrated to theToJsonObject() : JsonObject/Foo(JsonObject)API. The migration is mechanical and follows the patterns already established in this PR.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.files/resources/Analytical/*.JSONfixtures viaLibraryFixtureTests— all 14 library files round-trip.Newtonsoft.Jsonpackage reference in any.csproj.JObject/JArray/JToken/JValuetype references anywhere in*.csexcept inside comments.Notable commits (chronological)
462aa9ea183af25397efc0cb→f75ec4dabec023b8→7345481f988ccb51,79edd9c41d7c4f42,4dd4a3b2IJSAMObjectinterface flipped to JsonObject7592cc5f,95c53833,7e47484626d77a1e→5ee578a8da2eb248935e44ce