From f8290d4eaaf372d1dd7d9884acd844c5e41db823 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 May 2025 15:15:25 +0000 Subject: [PATCH 01/16] Initial plan for issue From 298bf174999d6c52123a80000464c648f3ff22c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 May 2025 15:24:25 +0000 Subject: [PATCH 02/16] Initial plan for removing Newtonsoft.Json in favor of System.Text.Json Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- Directory.Build.props | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index b14204e9a..a7d44d531 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -87,7 +87,7 @@ - 13 + 12 diff --git a/global.json b/global.json index c6fbdb46a..3f809cbf6 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100", + "version": "8.0.100", "rollForward": "feature" } } From 9cb2c6e1944c62149967ae9979e9ccbf1c19012f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 May 2025 15:28:19 +0000 Subject: [PATCH 03/16] Replace Newtonsoft.Json with System.Text.Json in tests and generator models Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../Converters/QualifiedNameConverter.cs | 13 +-- .../Converters/TypedQNameConverter.cs | 12 ++- .../OpenXmlGeneratorDataSource.cs | 10 +- .../PartConstraintRuleTests.cs | 14 ++- .../ParticleTests.cs | 93 +++++++------------ 5 files changed, 65 insertions(+), 77 deletions(-) diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs index 16a5febd1..144055fd3 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs +++ b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs @@ -2,25 +2,26 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Generator.Models; -using Newtonsoft.Json; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DocumentFormat.OpenXml.Generator.Converters; internal class QualifiedNameConverter : JsonConverter { - public override QName? ReadJson(JsonReader reader, Type objectType, QName? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override QName? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType != JsonToken.String) + if (reader.TokenType != JsonTokenType.String) { throw new InvalidOperationException("QName must be encoded as a string"); } - var str = serializer.Deserialize(reader) ?? string.Empty; - + var str = reader.GetString() ?? string.Empty; return QName.Parse(str); } - public override void WriteJson(JsonWriter writer, QName? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, QName value, JsonSerializerOptions options) { throw new NotImplementedException(); } diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs index acecdcdbc..70456f10b 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs +++ b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs @@ -2,20 +2,22 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Generator.Models; -using Newtonsoft.Json; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DocumentFormat.OpenXml.Generator.Converters; internal class TypedQNameConverter : JsonConverter { - public override TypedQName? ReadJson(JsonReader reader, Type objectType, TypedQName? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override TypedQName? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType != JsonToken.String) + if (reader.TokenType != JsonTokenType.String) { throw new InvalidOperationException("TypedQName must be encoded as a string"); } - var str = serializer.Deserialize(reader) ?? string.Empty; + var str = reader.GetString() ?? string.Empty; var split = str.Split('/'); if (split.Length != 2) @@ -26,7 +28,7 @@ internal class TypedQNameConverter : JsonConverter return new TypedQName(QName.Parse(split[0]), QName.Parse(split[1])); } - public override void WriteJson(JsonWriter writer, TypedQName? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, TypedQName value, JsonSerializerOptions options) { throw new NotImplementedException(); } diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs b/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs index 9ff0ef930..869fa3849 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs +++ b/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs @@ -4,25 +4,25 @@ using DocumentFormat.OpenXml.Generator.Converters; using DocumentFormat.OpenXml.Generator.Models; using DocumentFormat.OpenXml.Generator.Schematron; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DocumentFormat.OpenXml.Generator; public record OpenXmlGeneratorDataSource { - private static readonly JsonSerializerSettings _settings = new() + private static readonly JsonSerializerOptions _options = new() { Converters = { - new StringEnumConverter(), + new JsonStringEnumConverter(), new QualifiedNameConverter(), new TypedQNameConverter(), }, }; - public static T? Deserialize(string? content) => content is null ? default : JsonConvert.DeserializeObject(content, _settings); + public static T? Deserialize(string? content) => content is null ? default : JsonSerializer.Deserialize(content, _options); public ImmutableArray KnownNamespaces { get; init; } = ImmutableArray.Create(); diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs index b69c4359b..5743d95c0 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs @@ -4,14 +4,14 @@ using DocumentFormat.OpenXml.Features; using DocumentFormat.OpenXml.Framework; using DocumentFormat.OpenXml.Packaging; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using NSubstitute; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; using Xunit; namespace DocumentFormat.OpenXml.Tests @@ -157,7 +157,15 @@ private static OpenXmlPart InitializePart(Type type) using (var reader = new StreamReader(stream!)) { #nullable disable - return JsonConvert.DeserializeObject(reader.ReadToEnd(), new StringEnumConverter()) + var options = new JsonSerializerOptions + { + Converters = + { + new JsonStringEnumConverter(), + } + }; + + return JsonSerializer.Deserialize(reader.ReadToEnd(), options) .ToDictionary(t => t.Name, StringComparer.Ordinal); #nullable enable } diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs index c04911d81..f11472a14 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs @@ -3,15 +3,14 @@ using DocumentFormat.OpenXml.Framework; using DocumentFormat.OpenXml.Validation.Schema; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; using Xunit; namespace DocumentFormat.OpenXml.Packaging.Tests @@ -207,21 +206,20 @@ public void ValidateExpectedParticles() private void AssertEqual(Dictionary> constraints) { - var settings = new JsonSerializerSettings + var options = new JsonSerializerOptions { - Formatting = Formatting.Indented, - Converters = new JsonConverter[] + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { - new StringEnumConverter(), + new JsonStringEnumConverter(), new TypeNameConverter(), new QNameConverter(), - }, - ContractResolver = new OccursDefaultResolver(), - NullValueHandling = NullValueHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Ignore, + } }; - var serializer = JsonSerializer.Create(settings); + options.AddContext(); + var tmp = Path.GetTempFileName(); _output.WriteLine($"Writing output to {tmp}"); @@ -231,10 +229,11 @@ private void AssertEqual(Dictionary> { fs.SetLength(0); + var orderedData = constraints.OrderBy(t => t.Key.FullName, StringComparer.Ordinal); + var json = JsonSerializer.Serialize(orderedData, options); using (var textWriter = new StreamWriter(fs)) - using (var writer = new JsonTextWriter(textWriter) { Indentation = 1 }) { - serializer.Serialize(writer, constraints.OrderBy(t => t.Key.FullName, StringComparer.Ordinal)); + textWriter.Write(json); } } @@ -250,42 +249,12 @@ private void AssertEqual(Dictionary> } } - private class OccursDefaultResolver : DefaultContractResolver + [JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = true)] + [JsonSerializable(typeof(IEnumerable>>))] + private partial class OccursCustomContext : JsonSerializerContext { - protected override JsonContract CreateContract(Type objectType) - { - // CompositeParticle implements IEnumerable to enable collection initializers, but we want it to serialize as if it were just the object - if (objectType == typeof(CompositeParticle)) - { - return CreateObjectContract(objectType); - } - - return base.CreateContract(objectType); - } - - protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) - { - var properties = base.CreateProperties(type, memberSerialization); - - foreach (var prop in properties) - { - if (prop.PropertyName == nameof(ParticleConstraint.MinOccurs) || prop.PropertyName == nameof(ParticleConstraint.MaxOccurs)) - { - prop.DefaultValue = 1; - } - else if (prop.PropertyName == nameof(ParticleConstraint.Version) || prop.PropertyName == nameof(CompositeParticle.RequireFilter)) - { - prop.Ignored = true; - } - else if (prop.PropertyName == nameof(CompositeParticle.ChildrenParticles)) - { - prop.PropertyType = typeof(IEnumerable); - prop.ShouldSerialize = c => ((CompositeParticle)c).ChildrenParticles.Any(); - } - } - - return properties.OrderBy(p => p.PropertyName).ToList(); - } } private class VersionCollection : IEnumerable> @@ -324,22 +293,30 @@ public void Add(FileFormatVersions key, T value) IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - private class TypeNameConverter : JsonConverter + private sealed class TypeNameConverter : JsonConverter { - public override Type ReadJson(JsonReader reader, Type objectType, Type? existingValue, bool hasExistingValue, JsonSerializer serializer) - => throw new NotImplementedException(); + public override Type? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } - public override void WriteJson(JsonWriter writer, Type? value, JsonSerializer serializer) - => serializer.Serialize(writer, value!.FullName); + public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.FullName); + } } private sealed class QNameConverter : JsonConverter { - public override OpenXmlQualifiedName ReadJson(JsonReader reader, Type objectType, OpenXmlQualifiedName existingValue, bool hasExistingValue, JsonSerializer serializer) - => throw new NotImplementedException(); + public override OpenXmlQualifiedName? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } - public override void WriteJson(JsonWriter writer, OpenXmlQualifiedName value, JsonSerializer serializer) - => writer.WriteValue(value.ToString()); + public override void Write(Utf8JsonWriter writer, OpenXmlQualifiedName value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } } } } From 282fab426093db7631532d5df5ece70216726565 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 May 2025 15:29:58 +0000 Subject: [PATCH 04/16] Remove Newtonsoft.Json package references Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- Directory.Packages.props | 1 - .../DocumentFormat.OpenXml.Generator.Models.csproj | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a68403d60..fe05d46ac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,6 @@ - diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj b/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj index d5ebe540a..6a11c3d57 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj +++ b/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj @@ -9,7 +9,7 @@ DocumentFormat.OpenXml.Generator - + From b1dd0f452c6ab01f0fd0c8106d840f9bd2ddd34c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 May 2025 15:38:42 +0000 Subject: [PATCH 05/16] Update ParticleTests.cs JSON formatting Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- Directory.Packages.props | 1 + .../ParticleTests.cs | 19 +++++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fe05d46ac..a68403d60 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,7 @@ + diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs index f11472a14..6b5eb68d3 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs @@ -210,6 +210,7 @@ private void AssertEqual(Dictionary> { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = null, // Use Pascal case like Newtonsoft Converters = { new JsonStringEnumConverter(), @@ -218,8 +219,6 @@ private void AssertEqual(Dictionary> } }; - options.AddContext(); - var tmp = Path.GetTempFileName(); _output.WriteLine($"Writing output to {tmp}"); @@ -231,6 +230,12 @@ private void AssertEqual(Dictionary> var orderedData = constraints.OrderBy(t => t.Key.FullName, StringComparer.Ordinal); var json = JsonSerializer.Serialize(orderedData, options); + + // Fix JSON formatting to match Newtonsoft.Json indentation + json = json.Replace(" {", " {") + .Replace(" \"", " \"") + .Replace(" ", " "); + using (var textWriter = new StreamWriter(fs)) { textWriter.Write(json); @@ -249,13 +254,7 @@ private void AssertEqual(Dictionary> } } - [JsonSourceGenerationOptions( - PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, - WriteIndented = true)] - [JsonSerializable(typeof(IEnumerable>>))] - private partial class OccursCustomContext : JsonSerializerContext - { - } + private class VersionCollection : IEnumerable> { @@ -308,7 +307,7 @@ public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOpti private sealed class QNameConverter : JsonConverter { - public override OpenXmlQualifiedName? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override OpenXmlQualifiedName Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw new NotImplementedException(); } From 3a5700fe6a3b690b8a301775c8830145b6ab0ee0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:38:42 +0000 Subject: [PATCH 06/16] Revert generator models to use Newtonsoft.Json and fix StyleCop warnings Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../Converters/QualifiedNameConverter.cs | 13 ++++++------- .../Converters/TypedQNameConverter.cs | 12 +++++------- .../DocumentFormat.OpenXml.Generator.Models.csproj | 2 +- .../OpenXmlGeneratorDataSource.cs | 10 +++++----- .../PartConstraintRuleTests.cs | 4 ++-- .../ParticleTests.cs | 8 +++----- 6 files changed, 22 insertions(+), 27 deletions(-) diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs index 144055fd3..16a5febd1 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs +++ b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs @@ -2,26 +2,25 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Generator.Models; -using System; -using System.Text.Json; -using System.Text.Json.Serialization; +using Newtonsoft.Json; namespace DocumentFormat.OpenXml.Generator.Converters; internal class QualifiedNameConverter : JsonConverter { - public override QName? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override QName? ReadJson(JsonReader reader, Type objectType, QName? existingValue, bool hasExistingValue, JsonSerializer serializer) { - if (reader.TokenType != JsonTokenType.String) + if (reader.TokenType != JsonToken.String) { throw new InvalidOperationException("QName must be encoded as a string"); } - var str = reader.GetString() ?? string.Empty; + var str = serializer.Deserialize(reader) ?? string.Empty; + return QName.Parse(str); } - public override void Write(Utf8JsonWriter writer, QName value, JsonSerializerOptions options) + public override void WriteJson(JsonWriter writer, QName? value, JsonSerializer serializer) { throw new NotImplementedException(); } diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs index 70456f10b..acecdcdbc 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs +++ b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs @@ -2,22 +2,20 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Generator.Models; -using System; -using System.Text.Json; -using System.Text.Json.Serialization; +using Newtonsoft.Json; namespace DocumentFormat.OpenXml.Generator.Converters; internal class TypedQNameConverter : JsonConverter { - public override TypedQName? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TypedQName? ReadJson(JsonReader reader, Type objectType, TypedQName? existingValue, bool hasExistingValue, JsonSerializer serializer) { - if (reader.TokenType != JsonTokenType.String) + if (reader.TokenType != JsonToken.String) { throw new InvalidOperationException("TypedQName must be encoded as a string"); } - var str = reader.GetString() ?? string.Empty; + var str = serializer.Deserialize(reader) ?? string.Empty; var split = str.Split('/'); if (split.Length != 2) @@ -28,7 +26,7 @@ internal class TypedQNameConverter : JsonConverter return new TypedQName(QName.Parse(split[0]), QName.Parse(split[1])); } - public override void Write(Utf8JsonWriter writer, TypedQName value, JsonSerializerOptions options) + public override void WriteJson(JsonWriter writer, TypedQName? value, JsonSerializer serializer) { throw new NotImplementedException(); } diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj b/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj index 6a11c3d57..d5ebe540a 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj +++ b/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj @@ -9,7 +9,7 @@ DocumentFormat.OpenXml.Generator - + diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs b/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs index 869fa3849..9ff0ef930 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs +++ b/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs @@ -4,25 +4,25 @@ using DocumentFormat.OpenXml.Generator.Converters; using DocumentFormat.OpenXml.Generator.Models; using DocumentFormat.OpenXml.Generator.Schematron; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using System.Collections.Immutable; -using System.Text.Json; -using System.Text.Json.Serialization; namespace DocumentFormat.OpenXml.Generator; public record OpenXmlGeneratorDataSource { - private static readonly JsonSerializerOptions _options = new() + private static readonly JsonSerializerSettings _settings = new() { Converters = { - new JsonStringEnumConverter(), + new StringEnumConverter(), new QualifiedNameConverter(), new TypedQNameConverter(), }, }; - public static T? Deserialize(string? content) => content is null ? default : JsonSerializer.Deserialize(content, _options); + public static T? Deserialize(string? content) => content is null ? default : JsonConvert.DeserializeObject(content, _settings); public ImmutableArray KnownNamespaces { get; init; } = ImmutableArray.Create(); diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs index 5743d95c0..41f8920e8 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs @@ -162,9 +162,9 @@ private static OpenXmlPart InitializePart(Type type) Converters = { new JsonStringEnumConverter(), - } + }, }; - + return JsonSerializer.Deserialize(reader.ReadToEnd(), options) .ToDictionary(t => t.Name, StringComparer.Ordinal); #nullable enable diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs index 6b5eb68d3..9e02ce31c 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs @@ -216,7 +216,7 @@ private void AssertEqual(Dictionary> new JsonStringEnumConverter(), new TypeNameConverter(), new QNameConverter(), - } + }, }; var tmp = Path.GetTempFileName(); @@ -230,12 +230,12 @@ private void AssertEqual(Dictionary> var orderedData = constraints.OrderBy(t => t.Key.FullName, StringComparer.Ordinal); var json = JsonSerializer.Serialize(orderedData, options); - + // Fix JSON formatting to match Newtonsoft.Json indentation json = json.Replace(" {", " {") .Replace(" \"", " \"") .Replace(" ", " "); - + using (var textWriter = new StreamWriter(fs)) { textWriter.Write(json); @@ -254,8 +254,6 @@ private void AssertEqual(Dictionary> } } - - private class VersionCollection : IEnumerable> { private readonly List> _dic = new List>(); From ff723c102d4ecb5dc3c8ec47c9aefce71b4f773b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:46:02 +0000 Subject: [PATCH 07/16] Downgrade System.Collections.Immutable to 8.0.0 for source generator compatibility Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 12b0f3705..37b0369da 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From 029f016d21d5e41909766f481f746f4852f454e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:07:33 +0000 Subject: [PATCH 08/16] Revert LangVersion to 13 and SDK version to 9.0.100 Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- Directory.Build.props | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index a7d44d531..b14204e9a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -87,7 +87,7 @@ - 12 + 13 diff --git a/global.json b/global.json index 3f809cbf6..c6fbdb46a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "9.0.100", "rollForward": "feature" } } From e5dcca3cede176ca464efd6746fd2de0623fd551 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:14:52 +0000 Subject: [PATCH 09/16] Use native System.Text.Json indentation with Utf8JsonWriter Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../ParticleTests.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs index 9e02ce31c..9c8ccb090 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs @@ -229,16 +229,17 @@ private void AssertEqual(Dictionary> fs.SetLength(0); var orderedData = constraints.OrderBy(t => t.Key.FullName, StringComparer.Ordinal); - var json = JsonSerializer.Serialize(orderedData, options); - // Fix JSON formatting to match Newtonsoft.Json indentation - json = json.Replace(" {", " {") - .Replace(" \"", " \"") - .Replace(" ", " "); + // Use Utf8JsonWriter with custom indentation to match expected format + var writerOptions = new JsonWriterOptions + { + Indented = true, + IndentSize = 1, + }; - using (var textWriter = new StreamWriter(fs)) + using (var writer = new Utf8JsonWriter(fs, writerOptions)) { - textWriter.Write(json); + JsonSerializer.Serialize(writer, orderedData, options); } } From 319e2555585a8621a9dbbf033be12d651a875ec7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:39:31 +0000 Subject: [PATCH 10/16] Revert System.Collections.Immutable to 9.0.4 Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 37b0369da..12b0f3705 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From d3415c5b75e33fc13d9c15d1a837c07de0cc35f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:45:12 +0000 Subject: [PATCH 11/16] Remove conversion-related comment from ParticleTests.cs Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs index 9c8ccb090..a8e7ae30e 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs @@ -210,7 +210,7 @@ private void AssertEqual(Dictionary> { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNamingPolicy = null, // Use Pascal case like Newtonsoft + PropertyNamingPolicy = null, Converters = { new JsonStringEnumConverter(), From 41d62db2a446d840bc44c2e44b106b5632a3a85b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 20:39:52 +0000 Subject: [PATCH 12/16] Add ParticleConstraintConverter to replicate OccursDefaultResolver behavior Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../ParticleTests.cs | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs index a8e7ae30e..d317455af 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs @@ -209,13 +209,14 @@ private void AssertEqual(Dictionary> var options = new JsonSerializerOptions { WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, PropertyNamingPolicy = null, Converters = { new JsonStringEnumConverter(), new TypeNameConverter(), new QNameConverter(), + new ParticleConstraintConverter(), }, }; @@ -316,5 +317,63 @@ public override void Write(Utf8JsonWriter writer, OpenXmlQualifiedName value, Js writer.WriteStringValue(value.ToString()); } } + + private sealed class ParticleConstraintConverter : JsonConverter + { + public override ParticleConstraint? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, ParticleConstraint value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + // Get properties in alphabetical order + var properties = new List<(string Name, object? Value, bool ShouldWrite)>(); + + if (value is CompositeParticle composite) + { + // ChildrenParticles - only if not empty + if (composite.ChildrenParticles.Any()) + { + properties.Add(("ChildrenParticles", composite.ChildrenParticles, true)); + } + } + + // MaxOccurs - only if not default value of 1 + if (value.MaxOccurs != 1) + { + properties.Add(("MaxOccurs", value.MaxOccurs, true)); + } + + // MinOccurs - only if not default value of 1 + if (value.MinOccurs != 1) + { + properties.Add(("MinOccurs", value.MinOccurs, true)); + } + + // ParticleType + properties.Add(("ParticleType", value.ParticleType, true)); + + // Type - only for ElementParticle + if (value is ElementParticle element) + { + properties.Add(("Type", element.Type, true)); + } + + // Sort properties alphabetically and write them + foreach (var (name, propValue, shouldWrite) in properties.OrderBy(p => p.Name)) + { + if (shouldWrite && propValue != null) + { + writer.WritePropertyName(name); + JsonSerializer.Serialize(writer, propValue, propValue.GetType(), options); + } + } + + writer.WriteEndObject(); + } + } } } From 117a12a0db01b719091a88a9a23d45afffc51e75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:13:23 +0000 Subject: [PATCH 13/16] Fix ParticleConstraintConverter to handle AnyParticle and ElementParticle correctly Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../ParticleTests.cs | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs index d317455af..e82751a60 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs @@ -339,6 +339,33 @@ public override void Write(Utf8JsonWriter writer, ParticleConstraint value, Json { properties.Add(("ChildrenParticles", composite.ChildrenParticles, true)); } + + // ParticleType - for CompositeParticle + properties.Add(("ParticleType", value.ParticleType, true)); + } + else if (value is ElementParticle element) + { + // Type - only for ElementParticle + properties.Add(("Type", element.Type, true)); + } + else + { + // For AnyParticle, handle NamespaceValue + var anyParticleType = value.GetType(); + var namespaceValueProp = anyParticleType.GetProperty("NamespaceValue"); + if (namespaceValueProp != null) + { + var namespaceValue = namespaceValueProp.GetValue(value); + + // Only include NamespaceValue if it's not the default (Any = 0) + if (namespaceValue != null && Convert.ToInt32(namespaceValue) != 0) + { + properties.Add(("NamespaceValue", namespaceValue, true)); + } + } + + // ParticleType - for AnyParticle and other types + properties.Add(("ParticleType", value.ParticleType, true)); } // MaxOccurs - only if not default value of 1 @@ -353,15 +380,6 @@ public override void Write(Utf8JsonWriter writer, ParticleConstraint value, Json properties.Add(("MinOccurs", value.MinOccurs, true)); } - // ParticleType - properties.Add(("ParticleType", value.ParticleType, true)); - - // Type - only for ElementParticle - if (value is ElementParticle element) - { - properties.Add(("Type", element.Type, true)); - } - // Sort properties alphabetically and write them foreach (var (name, propValue, shouldWrite) in properties.OrderBy(p => p.Name)) { From 298724b0003682add96bce7fc7149a99ede36725 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 19:00:10 +0000 Subject: [PATCH 14/16] Update ParticleConstraintConverter to use reflection for all properties Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../ParticleTests.cs | 91 +++++++++++-------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs index e82751a60..42476ad7e 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs @@ -329,59 +329,76 @@ public override void Write(Utf8JsonWriter writer, ParticleConstraint value, Json { writer.WriteStartObject(); - // Get properties in alphabetical order - var properties = new List<(string Name, object? Value, bool ShouldWrite)>(); + // Get all public properties from the type + var type = value.GetType(); + var allProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.GetIndexParameters().Length == 0); - if (value is CompositeParticle composite) + var propertiesToWrite = new List<(string Name, object? Value, bool ShouldWrite)>(); + + foreach (var prop in allProperties) { - // ChildrenParticles - only if not empty - if (composite.ChildrenParticles.Any()) + var propName = prop.Name; + var propValue = prop.GetValue(value); + + // Ignore certain properties + if (propName == nameof(ParticleConstraint.Version) || + propName == "RequireFilter" || + propName == "ParticleValidator" || + propName == "UnboundedMaxOccurs" || + propName == "CanOccursMoreThanOne") { - properties.Add(("ChildrenParticles", composite.ChildrenParticles, true)); + continue; } - // ParticleType - for CompositeParticle - properties.Add(("ParticleType", value.ParticleType, true)); - } - else if (value is ElementParticle element) - { - // Type - only for ElementParticle - properties.Add(("Type", element.Type, true)); - } - else - { - // For AnyParticle, handle NamespaceValue - var anyParticleType = value.GetType(); - var namespaceValueProp = anyParticleType.GetProperty("NamespaceValue"); - if (namespaceValueProp != null) + // Handle MinOccurs and MaxOccurs - only include if not default value of 1 + if (propName == nameof(ParticleConstraint.MinOccurs) || propName == nameof(ParticleConstraint.MaxOccurs)) { - var namespaceValue = namespaceValueProp.GetValue(value); + if (propValue is int intValue && intValue != 1) + { + propertiesToWrite.Add((propName, propValue, true)); + } + + continue; + } - // Only include NamespaceValue if it's not the default (Any = 0) - if (namespaceValue != null && Convert.ToInt32(namespaceValue) != 0) + // Handle ChildrenParticles - only include if not empty + if (propName == "ChildrenParticles") + { + if (value is CompositeParticle composite && composite.ChildrenParticles.Any()) { - properties.Add(("NamespaceValue", namespaceValue, true)); + propertiesToWrite.Add((propName, composite.ChildrenParticles, true)); } + + continue; } - // ParticleType - for AnyParticle and other types - properties.Add(("ParticleType", value.ParticleType, true)); - } + // Handle NamespaceValue - only include if not the default (Any = 0) + if (propName == "NamespaceValue") + { + if (propValue != null && Convert.ToInt32(propValue) != 0) + { + propertiesToWrite.Add((propName, propValue, true)); + } - // MaxOccurs - only if not default value of 1 - if (value.MaxOccurs != 1) - { - properties.Add(("MaxOccurs", value.MaxOccurs, true)); - } + continue; + } - // MinOccurs - only if not default value of 1 - if (value.MinOccurs != 1) - { - properties.Add(("MinOccurs", value.MinOccurs, true)); + // For ElementParticle, don't include ParticleType + if (value is ElementParticle && propName == nameof(ParticleConstraint.ParticleType)) + { + continue; + } + + // Include all other properties + if (propValue != null) + { + propertiesToWrite.Add((propName, propValue, true)); + } } // Sort properties alphabetically and write them - foreach (var (name, propValue, shouldWrite) in properties.OrderBy(p => p.Name)) + foreach (var (name, propValue, shouldWrite) in propertiesToWrite.OrderBy(p => p.Name)) { if (shouldWrite && propValue != null) { From 4d5c446c39723b3ffd5d2df551a53879fce30240 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:10:51 +0000 Subject: [PATCH 15/16] Remove all Newtonsoft.Json references from codebase Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- Directory.Packages.props | 1 - .../Converters/QualifiedNameConverter.cs | 13 +++++++------ .../Converters/TypedQNameConverter.cs | 12 +++++++----- .../DocumentFormat.OpenXml.Generator.Models.csproj | 2 +- .../OpenXmlGeneratorDataSource.cs | 10 +++++----- .../SourceGenerator.targets | 5 +---- test/Directory.Build.targets | 1 - 7 files changed, 21 insertions(+), 23 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 12b0f3705..5de32fc7a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,6 @@ - diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs index 16a5febd1..144055fd3 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs +++ b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/QualifiedNameConverter.cs @@ -2,25 +2,26 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Generator.Models; -using Newtonsoft.Json; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DocumentFormat.OpenXml.Generator.Converters; internal class QualifiedNameConverter : JsonConverter { - public override QName? ReadJson(JsonReader reader, Type objectType, QName? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override QName? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType != JsonToken.String) + if (reader.TokenType != JsonTokenType.String) { throw new InvalidOperationException("QName must be encoded as a string"); } - var str = serializer.Deserialize(reader) ?? string.Empty; - + var str = reader.GetString() ?? string.Empty; return QName.Parse(str); } - public override void WriteJson(JsonWriter writer, QName? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, QName value, JsonSerializerOptions options) { throw new NotImplementedException(); } diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs index acecdcdbc..70456f10b 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs +++ b/gen/DocumentFormat.OpenXml.Generator.Models/Converters/TypedQNameConverter.cs @@ -2,20 +2,22 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Generator.Models; -using Newtonsoft.Json; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DocumentFormat.OpenXml.Generator.Converters; internal class TypedQNameConverter : JsonConverter { - public override TypedQName? ReadJson(JsonReader reader, Type objectType, TypedQName? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override TypedQName? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType != JsonToken.String) + if (reader.TokenType != JsonTokenType.String) { throw new InvalidOperationException("TypedQName must be encoded as a string"); } - var str = serializer.Deserialize(reader) ?? string.Empty; + var str = reader.GetString() ?? string.Empty; var split = str.Split('/'); if (split.Length != 2) @@ -26,7 +28,7 @@ internal class TypedQNameConverter : JsonConverter return new TypedQName(QName.Parse(split[0]), QName.Parse(split[1])); } - public override void WriteJson(JsonWriter writer, TypedQName? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, TypedQName value, JsonSerializerOptions options) { throw new NotImplementedException(); } diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj b/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj index d5ebe540a..6a11c3d57 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj +++ b/gen/DocumentFormat.OpenXml.Generator.Models/DocumentFormat.OpenXml.Generator.Models.csproj @@ -9,7 +9,7 @@ DocumentFormat.OpenXml.Generator - + diff --git a/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs b/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs index 9ff0ef930..869fa3849 100644 --- a/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs +++ b/gen/DocumentFormat.OpenXml.Generator.Models/OpenXmlGeneratorDataSource.cs @@ -4,25 +4,25 @@ using DocumentFormat.OpenXml.Generator.Converters; using DocumentFormat.OpenXml.Generator.Models; using DocumentFormat.OpenXml.Generator.Schematron; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DocumentFormat.OpenXml.Generator; public record OpenXmlGeneratorDataSource { - private static readonly JsonSerializerSettings _settings = new() + private static readonly JsonSerializerOptions _options = new() { Converters = { - new StringEnumConverter(), + new JsonStringEnumConverter(), new QualifiedNameConverter(), new TypedQNameConverter(), }, }; - public static T? Deserialize(string? content) => content is null ? default : JsonConvert.DeserializeObject(content, _settings); + public static T? Deserialize(string? content) => content is null ? default : JsonSerializer.Deserialize(content, _options); public ImmutableArray KnownNamespaces { get; init; } = ImmutableArray.Create(); diff --git a/gen/DocumentFormat.OpenXml.Generator/SourceGenerator.targets b/gen/DocumentFormat.OpenXml.Generator/SourceGenerator.targets index 048cd0fe8..6bc09548c 100644 --- a/gen/DocumentFormat.OpenXml.Generator/SourceGenerator.targets +++ b/gen/DocumentFormat.OpenXml.Generator/SourceGenerator.targets @@ -7,10 +7,7 @@ - - - - + diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index c1153f3f8..b83d51d43 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -19,7 +19,6 @@ - From f25b814c308ea421c36edf896104d8f2ab9ae248 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Thu, 11 Dec 2025 13:57:05 -0800 Subject: [PATCH 16/16] fix up tests to validate json --- .../TestUtility.cs | 14 +++++++++++ .../ITestOutputHelperExtenstions.cs | 6 ++--- .../PartConstraintRuleTests.cs | 19 +++++++++------ .../ParticleTests.cs | 23 ++++++------------- .../data/PartConstraintData.json | 20 ++++++++-------- 5 files changed, 46 insertions(+), 36 deletions(-) diff --git a/test/DocumentFormat.OpenXml.Framework.Tests/TestUtility.cs b/test/DocumentFormat.OpenXml.Framework.Tests/TestUtility.cs index 49c147202..c968a1834 100644 --- a/test/DocumentFormat.OpenXml.Framework.Tests/TestUtility.cs +++ b/test/DocumentFormat.OpenXml.Framework.Tests/TestUtility.cs @@ -4,8 +4,10 @@ using System; using System.IO; using System.Reflection; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using Xunit; namespace DocumentFormat.OpenXml.Framework.Tests { @@ -13,6 +15,7 @@ internal static class TestUtility { private static readonly JsonSerializerOptions _options = new() { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Converters = { new OpenXmlNamespaceConverter(), @@ -22,6 +25,17 @@ internal static class TestUtility WriteIndented = true, }; + public static void ValidateJsonFileContentsAreEqual(Stream stream1, Stream stream2) + { + using var reader1 = new StreamReader(stream1); + using var reader2 = new StreamReader(stream2); + + var expected = reader1.ReadToEnd().Replace("\r\n", "\n"); + var actual = reader2.ReadToEnd().Replace("\r\n", "\n"); + + Assert.Equal(expected, actual); + } + #nullable enable public static T? Deserialize(string name) diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/ITestOutputHelperExtenstions.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/ITestOutputHelperExtenstions.cs index 920e5df48..1a3c8c77b 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/ITestOutputHelperExtenstions.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/ITestOutputHelperExtenstions.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using DocumentFormat.OpenXml.Framework; using DocumentFormat.OpenXml.Framework.Tests; -using System; using System.IO; using Xunit; @@ -11,13 +9,15 @@ namespace DocumentFormat.OpenXml.Tests { internal static class ITestOutputHelperExtenstions { - public static void WriteObjectToTempFile(this ITestOutputHelper output, string name, T obj) + public static string WriteObjectToTempFile(this ITestOutputHelper output, string name, T obj) { var tmp = Path.GetTempFileName(); output.WriteLine($"Wrote {name} to temp path {tmp}"); File.WriteAllText(tmp, TestUtility.Serialize(obj)); + + return tmp; } } } diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs index 41f8920e8..0e6532c08 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/PartConstraintRuleTests.cs @@ -3,7 +3,9 @@ using DocumentFormat.OpenXml.Features; using DocumentFormat.OpenXml.Framework; +using DocumentFormat.OpenXml.Framework.Tests; using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Packaging.Tests; using NSubstitute; using System; using System.Collections.Generic; @@ -86,10 +88,9 @@ public void ValidatePart(Type partType) } Assert.NotNull(expectedConstraints.Parts); -#if DEBUG + _output.WriteObjectToTempFile("expected constraints", expectedConstraints.Parts.OrderBy(p => p.RelationshipType)); _output.WriteObjectToTempFile("actual constraints", constraints.Rules.OrderBy(p => p.RelationshipType).Select(p => new PartConstraintRule2(p))); -#endif Assert.Equal( expectedConstraints.Parts.OrderBy(p => p.RelationshipType), @@ -119,7 +120,13 @@ public void ExportData() }) .OrderBy(d => d.Name, StringComparer.Ordinal); - _output.WriteObjectToTempFile("typed parts", result); + var output = _output.WriteObjectToTempFile("typed parts", result); + + using (var expectedStream = typeof(ParticleTests).GetTypeInfo().Assembly.GetManifestResourceStream("DocumentFormat.OpenXml.Packaging.Tests.data.PartConstraintData.json")) + using (var actualStream = new FileStream(output, FileMode.Open, FileAccess.Read)) + { + TestUtility.ValidateJsonFileContentsAreEqual(expectedStream!, actualStream); + } } public static IEnumerable GetOpenXmlParts() => GetParts().Select(p => new[] { p }); @@ -134,9 +141,7 @@ private static IEnumerable GetParts() => typeof(SpreadsheetDocument) private static OpenXmlPart InitializePart(Type type) { -#nullable disable - var part = (OpenXmlPart)Activator.CreateInstance(type, true); -#nullable enable + var part = (OpenXmlPart)Activator.CreateInstance(type, true)!; var appType = Substitute.For(); appType.Type.Returns(ApplicationType.None); @@ -148,7 +153,7 @@ private static OpenXmlPart InitializePart(Type type) private static ConstraintData GetConstraintData(OpenXmlPart part) => _cachedConstraintData.Value[part.GetType().FullName!]; - private static Lazy> _cachedConstraintData = new Lazy>(() => + private static readonly Lazy> _cachedConstraintData = new(() => { var names = typeof(PartConstraintRuleTests).GetTypeInfo().Assembly.GetManifestResourceNames(); diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs index 42476ad7e..427c1f808 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/ParticleTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Framework; +using DocumentFormat.OpenXml.Framework.Tests; using DocumentFormat.OpenXml.Validation.Schema; using System; using System.Collections; @@ -177,9 +178,7 @@ public void ValidateExpectedParticles() if (constructor is not null) { -#nullable disable - var element = (OpenXmlElement)Activator.CreateInstance(type); -#nullable enable + var element = (OpenXmlElement)Activator.CreateInstance(type)!; if (version.AtLeast(element!.InitialVersion)) { @@ -231,28 +230,20 @@ private void AssertEqual(Dictionary> var orderedData = constraints.OrderBy(t => t.Key.FullName, StringComparer.Ordinal); - // Use Utf8JsonWriter with custom indentation to match expected format - var writerOptions = new JsonWriterOptions + using var writer = new Utf8JsonWriter(fs, new JsonWriterOptions { Indented = true, IndentSize = 1, - }; + }); - using (var writer = new Utf8JsonWriter(fs, writerOptions)) - { - JsonSerializer.Serialize(writer, orderedData, options); - } + JsonSerializer.Serialize(writer, orderedData, options); } using (var expectedStream = typeof(ParticleTests).GetTypeInfo().Assembly.GetManifestResourceStream("DocumentFormat.OpenXml.Packaging.Tests.data.Particles.json")) - using (var expectedStreamReader = new StreamReader(expectedStream!)) using (var actualStream = File.OpenRead(tmp)) - using (var actualStreamReader = new StreamReader(actualStream)) { - var expected = expectedStreamReader.ReadToEnd().Replace("\r\n", "\n"); - var actual = actualStreamReader.ReadToEnd().Replace("\r\n", "\n"); - - Assert.Equal(expected, actual); + Assert.NotNull(expectedStream); + TestUtility.ValidateJsonFileContentsAreEqual(expectedStream, actualStream); } } diff --git a/test/DocumentFormat.OpenXml.Packaging.Tests/data/PartConstraintData.json b/test/DocumentFormat.OpenXml.Packaging.Tests/data/PartConstraintData.json index 4997cc6fd..c64282e3e 100644 --- a/test/DocumentFormat.OpenXml.Packaging.Tests/data/PartConstraintData.json +++ b/test/DocumentFormat.OpenXml.Packaging.Tests/data/PartConstraintData.json @@ -811,6 +811,16 @@ "TargetPath": "externalReferences", "Parts": [] }, + { + "Name": "DocumentFormat.OpenXml.Packaging.FeaturePropertyBagsPart", + "ContentType": "application/vnd.ms-excel.featurepropertybag+xml", + "IsContentTypeFixed": true, + "RelationshipType": "http://schemas.microsoft.com/office/2022/11/relationships/FeaturePropertyBag", + "TargetFileExtension": ".xml", + "TargetName": "featurePropertyBag", + "TargetPath": "featurePropertyBag", + "Parts": [] + }, { "Name": "DocumentFormat.OpenXml.Packaging.FontPart", "ContentType": null, @@ -4235,15 +4245,5 @@ "TargetName": "sig", "TargetPath": "_xmlsignatures", "Parts": [] - }, - { - "Name": "DocumentFormat.OpenXml.Packaging.FeaturePropertyBagsPart", - "ContentType": "application/vnd.ms-excel.featurepropertybag+xml", - "IsContentTypeFixed": true, - "RelationshipType": "http://schemas.microsoft.com/office/2022/11/relationships/FeaturePropertyBag", - "TargetFileExtension": ".xml", - "TargetName": "featurePropertyBag", - "TargetPath": "featurePropertyBag", - "Parts": [] } ] \ No newline at end of file