using System.Text.Json; using System.Text.Json.Serialization.Metadata; namespace AIStudio.TrimSmokeTest; /// /// Builds the contract System.Text.Json uses for a root and for everything it holds. /// /// /// A type whose contract needs code the trimmer removed fails right here, before any value is read or /// written, just like it fails in AI Studio. A chat which lacks the property in question fails all the same. /// public static class JsonContractWalker { /// /// Returns a description of every contract which cannot be built, starting at the given root. /// /// The type to start with, and the options AI Studio uses for it. /// One line per failure, naming the path from the root to the type which failed. public static IReadOnlyList FindBrokenContracts(JsonRoot root) { // // The same System.Text.Json does with the first call of Serialize or Deserialize: // root.Options.MakeReadOnly(populateMissingResolver: true); var failures = new List(); Visit(root.Type, root.Type.Name, root.Options, [], failures); return failures; } private static void Visit(Type type, string path, JsonSerializerOptions options, HashSet visited, List failures) { if (!visited.Add(type)) return; JsonTypeInfo typeInfo; try { typeInfo = options.GetTypeInfo(type); } catch (Exception exception) when (exception is NotSupportedException or InvalidOperationException) { failures.Add($"{path}: {exception.Message}"); return; } foreach (var property in typeInfo.Properties) Visit(property.PropertyType, $"{path} → {property.Name}", options, visited, failures); if (typeInfo.KeyType is { } keyType) Visit(keyType, $"{path} → key", options, visited, failures); if (typeInfo.ElementType is { } elementType) Visit(elementType, $"{path} → element", options, visited, failures); if (typeInfo.PolymorphismOptions is { } polymorphism) foreach (var derivedType in polymorphism.DerivedTypes) Visit(derivedType.DerivedType, $"{path} → {derivedType.DerivedType.Name}", options, visited, failures); } }