The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.
A save file crosses a durable-data boundary
A durable save should describe game facts rather than retain live scene references. SaveDtoProbe records a schema version and a nonnegative score in a small serializable data-transfer object. Loading reconstructs data, validates its supported version and value, and only then assigns gameplay state. That boundary is valuable even for a tiny file: successful JSON parsing does not prove a score is valid or a file belongs to the current schema. Keep engine object identity, presentation state and durable identifiers separate when the save format grows beyond this demonstration.
Unity's JsonUtility uses Unity serialization rules, including field-based supported types. It is not a drop-in replacement for every feature of System.Text.Json or a general JSON document database. The real example uses a Serializable class with public fields, whereas the portable companion uses ordinary C# properties with a different serializer. The two demonstrate a similar boundary without promising identical JSON naming, supported shapes or missing-member behavior. Review the actual serializer and DTO rather than copying a property-only console class into Unity and assuming its persistence behavior is unchanged.
Portable C# companion: a property-based JSON round trip
The SharpRune companion creates Version one and Score twelve, serializes them, and prints {"Version":1,"Score":12}. It deserializes that text and checks for a nonnull object whose Version is one and Score is nonnegative. The second line is 12 because the data passes that pattern. No file is written and no Unity storage path is consulted. This trace checks an in-memory System.Text.Json round trip only; it says nothing about JsonUtility's field rules, filesystem permissions or the durability of a real Player save after application shutdown.
var data = new SaveDto { Version = 1, Score = 12 };
string text = JsonSerializer.Serialize(data); Console.WriteLine(text);
var loaded = JsonSerializer.Deserialize<SaveDto>(text);
Console.WriteLine(loaded is { Version: 1, Score: >= 0 } ? loaded.Score : -1);
sealed class SaveDto { public int Version { get; set; } public int Score { get; set; } }Unity Editor: fields, version checks and runtime paths
SaveDtoProbe.cs is the separate Unity Editor example. SavePath rejects an unavailable persistentDataPath and combines the supported runtime directory with sharprune-save.json. Save Now clamps the outgoing score and writes JsonUtility output; Load Now reads, deserializes, validates and then replaces the runtime score. Each action reports failures instead of silently claiming success. Test on a supported target in Play mode and then in a built Player. Paths differ across platforms, tvOS can return an empty persistent path, and web storage needs platform-specific persistence review rather than desktop-file assumptions.
[ContextMenu("Load Now")]
public void LoadNow()
{
if (!Application.isPlaying) return;
try
{
SaveData data = JsonUtility.FromJson<SaveData>(File.ReadAllText(SavePath()));
if (data == null || data.version != 1 || data.score < 0) throw new InvalidOperationException("Unsupported or invalid save.");
score = data.score; Debug.Log("Loaded score: " + score, this);
}
catch (Exception error) { Debug.LogWarning("Load failed: " + error.Message, this); }
}Deserialization is not domain validation
A syntactically valid document can still contain an unsupported version or negative score, so parsing and validation need different failure explanations. The example catches and reports both at its action boundary but is not a complete save system. Writing directly over the only file does not provide atomic replacement or a recovery copy, and a version field alone does not implement migration. Do not save GameObject instance IDs as durable references or assume an Inspector Min attribute validates imported data. Define stable domain identifiers, migration rules and recovery behavior before trusting a larger save format.
Two independent save-boundary experiments
First, independently change only the companion's outgoing Version to two and predict a second line of -1 even though serialization and deserialization succeed. Then restore version one and set Score negative to reach the same rejection through a different domain condition. Hint: the final pattern checks both constraints; a successful parser is only the preceding stage. Keep the two cases separate and explain what a user-facing loader should report for unsupported schema versus invalid gameplay data. This practice runs entirely in SharpRune and never touches an actual save file.
Second, in a disposable Unity project with no real user data, save twelve, change the runtime score and load to restore it. Then independently test a missing file, malformed JSON and an unsupported version in copies of that small save. Hint: inspect the actual persistentDataPath rather than an assumed Assets folder, and restore the good file between cases. Repeat on the intended Player. State what additional temporary-file, replacement and backup policy would be required before calling the operation crash-resistant; the supplied File.WriteAllText demonstration does not establish that guarantee.
Review compatibility, failures and durability
Review the save boundary as four steps: obtain a supported path, encode or decode the DTO, validate its domain and version, then commit acceptable state. Describe the current failure message and the missing migration or recovery policy without hiding either behind successful parsing. The portable JSON has uppercase property names and the Unity DTO uses lowercase fields, so label each trace with its actual serializer. Keep Editor evidence separate from Player storage evidence, and preserve disposable test data whenever deliberately corrupting files to investigate failure behavior.
- Use a versioned DTO with the chosen serializer's supported members.
- Reject invalid domain values after deserialization.
- Resolve persistentDataPath on the actual runtime target.
- Do not mistake direct overwrite for atomic, recoverable persistence.
Unity Editor example
Set up in Unity 6
- Attach SaveDtoProbe.cs in a Unity6.0 project and enter Play mode on a target with supported file access.
- Set score to 12 and invoke Save Now; use the actual persistentDataPath to inspect the local JSON if appropriate.
- Change the runtime score, invoke Load Now and inspect the restored score.
- Test missing, malformed and unsupported-version files in a disposable experiment, preserving any real user data.
- Repeat in the intended Player; review tvOS and Web storage caveats rather than assuming desktop file durability.
SaveDtoProbe.cs
Download Unity script ↗using System;
using System.IO;
using UnityEngine;
public sealed class SaveDtoProbe : MonoBehaviour
{
[SerializeField, Min(0)] private int score = 12;
[Serializable] private sealed class SaveData { public int version; public int score; }
private string SavePath()
{
if (string.IsNullOrEmpty(Application.persistentDataPath)) throw new InvalidOperationException("No persistent path on this target.");
return Path.Combine(Application.persistentDataPath, "sharprune-save.json");
}
[ContextMenu("Save Now")]
public void SaveNow()
{
if (!Application.isPlaying) return;
try { File.WriteAllText(SavePath(), JsonUtility.ToJson(new SaveData { version = 1, score = Mathf.Max(0, score) })); Debug.Log("Save written", this); }
catch (Exception error) { Debug.LogWarning("Save failed: " + error.Message, this); }
}
[ContextMenu("Load Now")]
public void LoadNow()
{
if (!Application.isPlaying) return;
try
{
SaveData data = JsonUtility.FromJson<SaveData>(File.ReadAllText(SavePath()));
if (data == null || data.version != 1 || data.score < 0) throw new InvalidOperationException("Unsupported or invalid save.");
score = data.score; Debug.Log("Loaded score: " + score, this);
}
catch (Exception error) { Debug.LogWarning("Load failed: " + error.Message, this); }
}
}
What to observe in Unity
On a supported target, Save Now writes a small versioned JsonUtility DTO and Load Now validates and restores score. File or schema failures are reported; cross-platform persistence is not assumed.
The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
var data = new SaveDto { Version = 1, Score = 12 };
string text = JsonSerializer.Serialize(data); Console.WriteLine(text);
var loaded = JsonSerializer.Deserialize<SaveDto>(text);
Console.WriteLine(loaded is { Version: 1, Score: >= 0 } ? loaded.Score : -1);
sealed class SaveDto { public int Version { get; set; } public int Score { get; set; } }
C# practice output
{"Version":1,"Score":12}
12The console serializer is not JsonUtility, and no companion code writes a Unity save file.