From 6f4b7aa270f7a413f1cc396db80a358bd9c66c0b Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Mon, 24 Nov 2025 20:55:45 -0800 Subject: [PATCH 1/3] Migrate simple JSON serialization from Newtonsoft.Json to System.Text.Json Convert `FileWritingService`, `ScanCommand`, Mariner2ArtifactFilter, and `DetectorProcessingService` to use `System.Text.Json` for serialization. `LinuxScanner` retained on Newtonsoft for now due to `SyftOutput` union types requiring custom converters. --- .../FileWritingService.cs | 26 +++++++++---------- .../linux/Filters/Mariner2ArtifactFilter.cs | 4 +-- .../Commands/ScanCommand.cs | 12 ++++----- .../Services/DetectorProcessingService.cs | 4 +-- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Common/FileWritingService.cs b/src/Microsoft.ComponentDetection.Common/FileWritingService.cs index 0aff9fb7d..31b1d3412 100644 --- a/src/Microsoft.ComponentDetection.Common/FileWritingService.cs +++ b/src/Microsoft.ComponentDetection.Common/FileWritingService.cs @@ -5,10 +5,11 @@ namespace Microsoft.ComponentDetection.Common; using System.Collections.Concurrent; using System.Globalization; using System.IO; +using System.Text.Encodings.Web; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.ComponentDetection.Common.Exceptions; -using Newtonsoft.Json; /// public sealed class FileWritingService : IFileWritingService @@ -17,6 +18,13 @@ public sealed class FileWritingService : IFileWritingService /// The format string used to generate the timestamp for the manifest file. /// public const string TimestampFormatString = "yyyyMMddHHmmssfff"; + + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true, + }; + private readonly ConcurrentDictionary bufferedStreams = new(); private readonly object lockObject = new(); @@ -50,11 +58,8 @@ public void AppendToFile(string relativeFilePath, T obj) _ = this.bufferedStreams.TryAdd(relativeFilePath, streamWriter); } - var serializer = new JsonSerializer - { - Formatting = Formatting.Indented, - }; - serializer.Serialize(streamWriter, obj); + var jsonString = JsonSerializer.Serialize(obj, JsonSerializerOptions); + streamWriter.Write(jsonString); } /// @@ -79,13 +84,8 @@ public async Task WriteFileAsync(string relativeFilePath, string text, Cancellat /// public void WriteFile(FileInfo relativeFilePath, T obj) { - using var streamWriter = new StreamWriter(relativeFilePath.FullName); - using var jsonWriter = new JsonTextWriter(streamWriter); - var serializer = new JsonSerializer - { - Formatting = Formatting.Indented, - }; - serializer.Serialize(jsonWriter, obj); + using var stream = relativeFilePath.Create(); + JsonSerializer.Serialize(stream, obj, JsonSerializerOptions); } /// diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/Filters/Mariner2ArtifactFilter.cs b/src/Microsoft.ComponentDetection.Detectors/linux/Filters/Mariner2ArtifactFilter.cs index 6e02c714b..f772cc13a 100644 --- a/src/Microsoft.ComponentDetection.Detectors/linux/Filters/Mariner2ArtifactFilter.cs +++ b/src/Microsoft.ComponentDetection.Detectors/linux/Filters/Mariner2ArtifactFilter.cs @@ -3,9 +3,9 @@ namespace Microsoft.ComponentDetection.Detectors.Linux.Filters; using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using Microsoft.ComponentDetection.Common.Telemetry.Records; using Microsoft.ComponentDetection.Detectors.Linux.Contracts; -using Newtonsoft.Json; /// /// Filters out invalid ELF binary packages from Mariner 2.0 images that lack proper release/epoch version fields. @@ -50,7 +50,7 @@ public IEnumerable Filter(IEnumerable artifact artifactsList.Remove(elfArtifact); } - syftTelemetryRecord.ComponentsRemoved = JsonConvert.SerializeObject(removedComponents); + syftTelemetryRecord.ComponentsRemoved = JsonSerializer.Serialize(removedComponents); } return artifactsList; diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Commands/ScanCommand.cs b/src/Microsoft.ComponentDetection.Orchestrator/Commands/ScanCommand.cs index 3447ae185..3c34df542 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Commands/ScanCommand.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Commands/ScanCommand.cs @@ -3,13 +3,13 @@ namespace Microsoft.ComponentDetection.Orchestrator.Commands; using System; using System.IO; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.ComponentDetection.Common; using Microsoft.ComponentDetection.Contracts.BcdeModels; using Microsoft.ComponentDetection.Orchestrator.Services; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using Spectre.Console.Cli; /// @@ -18,6 +18,8 @@ namespace Microsoft.ComponentDetection.Orchestrator.Commands; public sealed class ScanCommand : AsyncCommand { private const string ManifestRelativePath = "ScanManifest_{timestamp}.json"; + private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true }; + private readonly IFileWritingService fileWritingService; private readonly IScanExecutionService scanExecutionService; private readonly ILogger logger; @@ -89,12 +91,8 @@ private void WriteComponentManifest(ScanSettings settings, ScanResult scanResult if (settings.PrintManifest) { - var jsonWriter = new JsonTextWriter(Console.Out); - var serializer = new JsonSerializer - { - Formatting = Formatting.Indented, - }; - serializer.Serialize(jsonWriter, scanResult); + var jsonString = JsonSerializer.Serialize(scanResult, IndentedJsonOptions); + Console.WriteLine(jsonString); } } } diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingService.cs index c60fc9d5d..7c4279bc2 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingService.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingService.cs @@ -7,6 +7,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Services; using System.Diagnostics; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using DotNet.Globbing; @@ -18,7 +19,6 @@ namespace Microsoft.ComponentDetection.Orchestrator.Services; using Microsoft.ComponentDetection.Orchestrator.Commands; using Microsoft.ComponentDetection.Orchestrator.Experiments; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using Spectre.Console; using static System.Environment; @@ -115,7 +115,7 @@ public async Task ProcessDetectorsAsync( resultCode = result.ResultCode; containerDetails = result.ContainerDetails; - record.AdditionalTelemetryDetails = result.AdditionalTelemetryDetails != null ? JsonConvert.SerializeObject(result.AdditionalTelemetryDetails) : null; + record.AdditionalTelemetryDetails = result.AdditionalTelemetryDetails != null ? JsonSerializer.Serialize(result.AdditionalTelemetryDetails) : null; record.IsExperimental = isExperimentalDetector; record.DetectorId = detector.Id; record.DetectedComponentCount = detectedComponents.Count(); From 788c53ef8df4778c55f3b5c79bf84fd1885e5f4b Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Mon, 24 Nov 2025 21:10:56 -0800 Subject: [PATCH 2/3] Add `[JsonPropertyName]` attributes to `Type` and `PackageUrl` --- .../BcdeModels/DockerLayer.cs | 8 ++++---- .../TypedComponent/CargoComponent.cs | 12 +++++------- .../TypedComponent/ConanComponent.cs | 2 ++ .../TypedComponent/CondaComponent.cs | 1 + .../TypedComponent/DockerImageComponent.cs | 1 + .../TypedComponent/DockerReferenceComponent.cs | 1 + .../TypedComponent/DotNetComponent.cs | 1 + .../TypedComponent/GitComponent.cs | 1 + .../TypedComponent/GoComponent.cs | 2 ++ .../TypedComponent/LinuxComponent.cs | 2 ++ .../TypedComponent/MavenComponent.cs | 2 ++ .../TypedComponent/NpmComponent.cs | 2 ++ .../TypedComponent/NugetComponent.cs | 2 ++ .../TypedComponent/OtherComponent.cs | 1 + .../TypedComponent/PipComponent.cs | 9 ++++----- .../TypedComponent/PodComponent.cs | 2 ++ .../TypedComponent/RubyGemsComponent.cs | 2 ++ .../TypedComponent/SpdxComponent.cs | 1 + .../TypedComponent/SwiftComponent.cs | 2 ++ .../TypedComponent/TypedComponent.cs | 3 +++ .../TypedComponent/VcpkgComponent.cs | 2 ++ 21 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DockerLayer.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DockerLayer.cs index c38aef21f..e5bf27cc7 100644 --- a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DockerLayer.cs +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/DockerLayer.cs @@ -7,22 +7,22 @@ public class DockerLayer { // Summary: // the command/script that was executed in order to create the layer. - [JsonPropertyName("createdBy")] + [JsonPropertyName("CreatedBy")] public string CreatedBy { get; set; } // Summary: // The Layer hash (docker inspect) that represents the changes between this layer and the previous layer - [JsonPropertyName("diffId")] + [JsonPropertyName("DiffId")] public string DiffId { get; set; } // Summary: // Whether or not this layer was found in the base image of the container - [JsonPropertyName("isBaseImage")] + [JsonPropertyName("IsBaseImage")] public bool IsBaseImage { get; set; } // Summary: // 0-indexed monotonically increasing ID for the order of the layer in the container. // Note: only includes non-empty layers - [JsonPropertyName("layerIndex")] + [JsonPropertyName("LayerIndex")] public int LayerIndex { get; set; } } diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CargoComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CargoComponent.cs index dd90adf24..c915e8b29 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CargoComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CargoComponent.cs @@ -2,7 +2,6 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent; using System.Text.Json.Serialization; -using Newtonsoft.Json; using PackageUrl; public class CargoComponent : TypedComponent @@ -28,24 +27,23 @@ public CargoComponent(string name, string version, string author = null, string public string Version { get; set; } #nullable enable - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] // Newtonsoft.Json - [System.Text.Json.Serialization.JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] // System.Text.Json + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("author")] public string? Author { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] // Newtonsoft.Json - [System.Text.Json.Serialization.JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] // System.Text.Json + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("license")] public string? License { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] // Newtonsoft.Json - [System.Text.Json.Serialization.JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] // System.Text.Json + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] public string? Source { get; set; } #nullable disable + [JsonIgnore] public override ComponentType Type => ComponentType.Cargo; + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl => new PackageURL("cargo", string.Empty, this.Name, this.Version, null, string.Empty); protected override string ComputeId() => $"{this.Name} {this.Version} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ConanComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ConanComponent.cs index c1f6a94a1..b2ca24b16 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ConanComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ConanComponent.cs @@ -34,8 +34,10 @@ public ConanComponent(string name, string version, string previous, string packa [JsonPropertyName("packageSourceURL")] public string PackageSourceURL => $"https://conan.io/center/recipes/{this.Name}?version={this.Version}"; + [JsonIgnore] public override ComponentType Type => ComponentType.Conan; + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl => new PackageURL("conan", string.Empty, this.Name, this.Version, null, string.Empty); protected override string ComputeId() => $"{this.Name} {this.Version} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs index 0c526e8d0..f77ee5ddd 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs @@ -46,6 +46,7 @@ public CondaComponent() [JsonPropertyName("mD5")] public string MD5 { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.Conda; protected override string ComputeId() => $"{this.Name} {this.Version} {this.Build} {this.Channel} {this.Subdir} {this.Namespace} {this.Url} {this.MD5} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerImageComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerImageComponent.cs index 7b6e0e6d7..30679a7bf 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerImageComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerImageComponent.cs @@ -26,6 +26,7 @@ public DockerImageComponent(string hash, string name = null, string tag = null) [JsonPropertyName("tag")] public string Tag { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.DockerImage; protected override string ComputeId() => $"{this.Name} {this.Tag} {this.Digest}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerReferenceComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerReferenceComponent.cs index 308a64e42..0e2c8f3cc 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerReferenceComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DockerReferenceComponent.cs @@ -33,6 +33,7 @@ public DockerReferenceComponent() [JsonPropertyName("domain")] public string Domain { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.DockerReference; public DockerReference FullReference diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DotNetComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DotNetComponent.cs index bb5bdda35..81694c41e 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DotNetComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/DotNetComponent.cs @@ -43,6 +43,7 @@ public DotNetComponent(string sdkVersion, string targetFramework = null, string [JsonPropertyName("projectType")] public string ProjectType { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.DotNet; /// diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GitComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GitComponent.cs index 407aa2106..ab250a7a9 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GitComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GitComponent.cs @@ -29,6 +29,7 @@ public GitComponent() [JsonPropertyName("tag")] public string Tag { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.Git; protected override string ComputeId() => $"{this.RepositoryUrl} : {this.CommitHash} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GoComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GoComponent.cs index 01d1f0085..4dedacf8a 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GoComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/GoComponent.cs @@ -37,8 +37,10 @@ public GoComponent() // Commit should be used in place of version when available // https://github.com/package-url/purl-spec/blame/180c46d266c45aa2bd81a2038af3f78e87bb4a25/README.rst#L610 + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl => new PackageURL("golang", null, this.Name, string.IsNullOrWhiteSpace(this.Hash) ? this.Version : this.Hash, null, null); + [JsonIgnore] public override ComponentType Type => ComponentType.Go; protected override string ComputeId() => $"{this.Name} {this.Version} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs index 8f4e924f5..f58bf8684 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/LinuxComponent.cs @@ -42,8 +42,10 @@ public LinuxComponent(string distribution, string release, string name, string v public string? Author { get; set; } #nullable disable + [JsonIgnore] public override ComponentType Type => ComponentType.Linux; + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl { get diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/MavenComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/MavenComponent.cs index acf4a71d9..0d3042f25 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/MavenComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/MavenComponent.cs @@ -27,8 +27,10 @@ public MavenComponent() [JsonPropertyName("version")] public string Version { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.Maven; + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl => new PackageURL("maven", this.GroupId, this.ArtifactId, this.Version, null, null); protected override string ComputeId() => $"{this.GroupId} {this.ArtifactId} {this.Version} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NpmComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NpmComponent.cs index f110af0c7..653349f20 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NpmComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NpmComponent.cs @@ -32,8 +32,10 @@ public NpmComponent(string name, string version, string hash = null, NpmAuthor a [JsonPropertyName("author")] public NpmAuthor Author { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.Npm; + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl => new PackageURL("npm", null, this.Name, this.Version, null, null); protected override string ComputeId() => $"{this.Name} {this.Version} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NugetComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NugetComponent.cs index 1827edf58..93b1410ba 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NugetComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/NugetComponent.cs @@ -27,8 +27,10 @@ public NuGetComponent(string name, string version, string[] authors = null) [JsonPropertyName("authors")] public string[] Authors { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.NuGet; + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl => new PackageURL("nuget", null, this.Name, this.Version, null, null); protected override string ComputeId() => $"{this.Name} {this.Version} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/OtherComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/OtherComponent.cs index 338cb194f..4d50c57e6 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/OtherComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/OtherComponent.cs @@ -31,6 +31,7 @@ public OtherComponent(string name, string version, Uri downloadUrl, string hash) [JsonPropertyName("hash")] public string Hash { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.Other; protected override string ComputeId() => $"{this.Name} {this.Version} {this.DownloadUrl} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs index fbeff8b57..46c85caa3 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs @@ -3,7 +3,6 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using Newtonsoft.Json; using PackageUrl; public class PipComponent : TypedComponent @@ -28,19 +27,19 @@ public PipComponent(string name, string version, string author = null, string li public string Version { get; set; } #nullable enable - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] // Newtonsoft.Json - [System.Text.Json.Serialization.JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] // System.Text.Json + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("author")] public string? Author { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] // Newtonsoft.Json - [System.Text.Json.Serialization.JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] // System.Text.Json + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("license")] public string? License { get; set; } #nullable disable + [JsonIgnore] public override ComponentType Type => ComponentType.Pip; + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl => new PackageURL("pypi", null, this.Name, this.Version, null, null); [SuppressMessage("Usage", "CA1308:Normalize String to Uppercase", Justification = "Casing cannot be overwritten.")] diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PodComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PodComponent.cs index 6b9c0fb91..07ff2eb83 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PodComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PodComponent.cs @@ -28,8 +28,10 @@ public PodComponent(string name, string version, string specRepo = "") [JsonPropertyName("specRepo")] public string SpecRepo { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.Pod; + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl { get diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/RubyGemsComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/RubyGemsComponent.cs index 3fe3f7720..57d05f1c2 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/RubyGemsComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/RubyGemsComponent.cs @@ -27,8 +27,10 @@ public RubyGemsComponent(string name, string version, string source = "") [JsonPropertyName("source")] public string Source { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.RubyGems; + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl => new PackageURL("gem", null, this.Name, this.Version, null, null); protected override string ComputeId() => $"{this.Name} {this.Version} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SpdxComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SpdxComponent.cs index cb7e532ed..e84bd6964 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SpdxComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SpdxComponent.cs @@ -21,6 +21,7 @@ public SpdxComponent(string spdxVersion, Uri documentNamespace, string name, str this.Path = this.ValidateRequiredInput(path, nameof(this.Path), nameof(ComponentType.Spdx)); } + [JsonIgnore] public override ComponentType Type => ComponentType.Spdx; [JsonPropertyName("rootElementId")] diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs index 884293b10..90b682280 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs @@ -37,12 +37,14 @@ public SwiftComponent(string name, string version, string packageUrl, string has [JsonPropertyName("version")] public string Version { get; } + [JsonIgnore] public override ComponentType Type => ComponentType.Swift; // Example PackageURL -> pkg:swift/github.com/apple/swift-asn1 // type: swift // namespace: github.com/apple // name: swift-asn1 + [JsonPropertyName("packageUrl")] public PackageURL PackageURL => new PackageURL( type: "swift", @namespace: this.GetNamespaceFromPackageUrl(), diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/TypedComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/TypedComponent.cs index e753c6dfe..206825e93 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/TypedComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/TypedComponent.cs @@ -54,8 +54,11 @@ internal TypedComponent() public abstract ComponentType Type { get; } /// Gets the id of the component. + [JsonProperty("id")] // Newtonsoft.Json + [JsonPropertyName("id")] // System.Text.Json public string Id => this.id ??= this.ComputeId(); + [JsonPropertyName("packageUrl")] public virtual PackageURL PackageUrl { get; } [JsonIgnore] // Newtonsoft.Json diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/VcpkgComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/VcpkgComponent.cs index 497b9f76b..30053aac8 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/VcpkgComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/VcpkgComponent.cs @@ -45,8 +45,10 @@ public VcpkgComponent(string spdxid, string name, string version, string triplet [JsonPropertyName("portVersion")] public int PortVersion { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.Vcpkg; + [JsonPropertyName("packageUrl")] public override PackageURL PackageUrl { get From 9f710175808302d747b55a620ca701113652fe72 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Tue, 25 Nov 2025 09:17:33 -0800 Subject: [PATCH 3/3] `System.Text.Json` does not automatically serialize derived class properties when the compile-time type is a base class --- .../FileWritingService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Common/FileWritingService.cs b/src/Microsoft.ComponentDetection.Common/FileWritingService.cs index 31b1d3412..59616c5c0 100644 --- a/src/Microsoft.ComponentDetection.Common/FileWritingService.cs +++ b/src/Microsoft.ComponentDetection.Common/FileWritingService.cs @@ -58,7 +58,7 @@ public void AppendToFile(string relativeFilePath, T obj) _ = this.bufferedStreams.TryAdd(relativeFilePath, streamWriter); } - var jsonString = JsonSerializer.Serialize(obj, JsonSerializerOptions); + var jsonString = JsonSerializer.Serialize(obj, obj.GetType(), JsonSerializerOptions); streamWriter.Write(jsonString); } @@ -85,7 +85,9 @@ public async Task WriteFileAsync(string relativeFilePath, string text, Cancellat public void WriteFile(FileInfo relativeFilePath, T obj) { using var stream = relativeFilePath.Create(); - JsonSerializer.Serialize(stream, obj, JsonSerializerOptions); + + // Use runtime type to ensure derived class properties are serialized + JsonSerializer.Serialize(stream, obj, obj.GetType(), JsonSerializerOptions); } ///