// // Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later. // using System.Collections.Immutable; using System.Text.Json; using Microsoft.Extensions.Logging; using StellaOps.Spdx3.JsonLd; using StellaOps.Spdx3.Model; using StellaOps.Spdx3.Model.Build; using StellaOps.Spdx3.Model.Security; using StellaOps.Spdx3.Model.Software; namespace StellaOps.Spdx3; /// /// SPDX 3.0.1 JSON-LD parser implementation. /// public sealed class Spdx3Parser : ISpdx3Parser { private readonly ISpdx3ContextResolver _contextResolver; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; /// /// Initializes a new instance of the class. /// public Spdx3Parser( ISpdx3ContextResolver contextResolver, ILogger logger, TimeProvider? timeProvider = null) { _contextResolver = contextResolver; _logger = logger; _timeProvider = timeProvider ?? TimeProvider.System; } /// public async Task ParseAsync( Stream stream, CancellationToken cancellationToken = default) { try { using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken) .ConfigureAwait(false); return await ParseDocumentAsync(document, cancellationToken) .ConfigureAwait(false); } catch (JsonException ex) { _logger.LogError(ex, "Failed to parse SPDX 3.0.1 JSON"); return Spdx3ParseResult.Failed("JSON_PARSE_ERROR", ex.Message, ex.Path); } } /// public async Task ParseAsync( string filePath, CancellationToken cancellationToken = default) { if (!File.Exists(filePath)) { return Spdx3ParseResult.Failed("FILE_NOT_FOUND", $"File not found: {filePath}"); } await using var stream = File.OpenRead(filePath); return await ParseAsync(stream, cancellationToken).ConfigureAwait(false); } /// public async Task ParseFromJsonAsync( string json, CancellationToken cancellationToken = default) { using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json)); return await ParseAsync(stream, cancellationToken).ConfigureAwait(false); } private async Task ParseDocumentAsync( JsonDocument document, CancellationToken cancellationToken) { var root = document.RootElement; var errors = new List(); var warnings = new List(); // Check for @context (JSON-LD indicator) if (!root.TryGetProperty("@context", out var contextElement)) { return Spdx3ParseResult.Failed("MISSING_CONTEXT", "No @context found. Is this an SPDX 3.0.1 JSON-LD document?"); } // Resolve context (for future expansion support) var contextRef = GetContextReference(contextElement); if (!string.IsNullOrEmpty(contextRef)) { await _contextResolver.ResolveAsync(contextRef, cancellationToken).ConfigureAwait(false); } // Parse @graph (main element array) var elements = new List(); var creationInfos = new List(); var profiles = new HashSet(); Spdx3SpdxDocument? spdxDocument = null; if (root.TryGetProperty("@graph", out var graphElement) && graphElement.ValueKind == JsonValueKind.Array) { foreach (var element in graphElement.EnumerateArray()) { var parsed = ParseElement(element, errors, warnings); if (parsed != null) { elements.Add(parsed); if (parsed is Spdx3SpdxDocument doc) { spdxDocument = doc; } } // Also extract CreationInfo var creationInfo = ExtractCreationInfo(element, errors); if (creationInfo != null) { creationInfos.Add(creationInfo); foreach (var profile in creationInfo.Profile) { profiles.Add(profile); } } } } else { // Single document format (no @graph) var parsed = ParseElement(root, errors, warnings); if (parsed != null) { elements.Add(parsed); if (parsed is Spdx3SpdxDocument doc) { spdxDocument = doc; } } } if (errors.Count > 0) { return Spdx3ParseResult.Failed(errors, warnings); } var result = new Spdx3Document(elements, creationInfos, profiles, spdxDocument); return Spdx3ParseResult.Succeeded(result, warnings); } private Spdx3Element? ParseElement( JsonElement element, List errors, List warnings) { if (element.ValueKind != JsonValueKind.Object) { return null; } // Get @type to determine element type var type = GetStringProperty(element, "@type") ?? GetStringProperty(element, "type"); if (string.IsNullOrEmpty(type)) { return null; } // Get spdxId (required) var spdxId = GetStringProperty(element, "spdxId") ?? GetStringProperty(element, "@id"); if (string.IsNullOrEmpty(spdxId)) { warnings.Add(new Spdx3ParseWarning("MISSING_SPDX_ID", $"Element of type {type} has no spdxId")); return null; } return type switch { "software_Package" or "Package" or "spdx:software_Package" => ParsePackage(element, spdxId), "software_File" or "File" or "spdx:software_File" => ParseFile(element, spdxId), "software_Snippet" or "Snippet" or "spdx:software_Snippet" => ParseSnippet(element, spdxId), "software_SpdxDocument" or "SpdxDocument" or "spdx:software_SpdxDocument" => ParseSpdxDocument(element, spdxId), "Relationship" or "spdx:Relationship" => ParseRelationship(element, spdxId), "Person" or "spdx:Person" => ParseAgent(element, spdxId), "Organization" or "spdx:Organization" => ParseAgent(element, spdxId), "Tool" or "spdx:Tool" => ParseAgent(element, spdxId), "Build" or "build_Build" or "spdx:Build" => ParseBuild(element, spdxId), "security_Vulnerability" or "Vulnerability" or "spdx:security_Vulnerability" => ParseVulnerability(element, spdxId), "security_VexAffectedVulnAssessmentRelationship" or "security_VexNotAffectedVulnAssessmentRelationship" or "security_VexFixedVulnAssessmentRelationship" or "security_VexUnderInvestigationVulnAssessmentRelationship" => ParseVexAssessment(element, spdxId, type), "security_CvssV3VulnAssessmentRelationship" => ParseCvssAssessment(element, spdxId), "security_EpssVulnAssessmentRelationship" => ParseEpssAssessment(element, spdxId), _ => ParseGenericElement(element, spdxId, type, warnings) }; } private Spdx3Package ParsePackage(JsonElement element, string spdxId) { return new Spdx3Package { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = GetStringProperty(element, "name"), Summary = GetStringProperty(element, "summary"), Description = GetStringProperty(element, "description"), Comment = GetStringProperty(element, "comment"), PackageVersion = GetStringProperty(element, "packageVersion"), DownloadLocation = GetStringProperty(element, "downloadLocation"), PackageUrl = GetStringProperty(element, "packageUrl"), HomePage = GetStringProperty(element, "homePage"), SourceInfo = GetStringProperty(element, "sourceInfo"), CopyrightText = GetStringProperty(element, "copyrightText"), SuppliedBy = GetStringProperty(element, "suppliedBy"), VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element), OriginatedBy = GetStringArrayProperty(element, "originatedBy"), AttributionText = GetStringArrayProperty(element, "attributionText") }; } private Spdx3File ParseFile(JsonElement element, string spdxId) { return new Spdx3File { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = GetStringProperty(element, "name") ?? string.Empty, Summary = GetStringProperty(element, "summary"), Description = GetStringProperty(element, "description"), Comment = GetStringProperty(element, "comment"), ContentType = GetStringProperty(element, "contentType"), CopyrightText = GetStringProperty(element, "copyrightText"), VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element) }; } private Spdx3Snippet ParseSnippet(JsonElement element, string spdxId) { return new Spdx3Snippet { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = GetStringProperty(element, "name"), Summary = GetStringProperty(element, "summary"), Description = GetStringProperty(element, "description"), Comment = GetStringProperty(element, "comment"), SnippetFromFile = GetStringProperty(element, "snippetFromFile") ?? string.Empty, CopyrightText = GetStringProperty(element, "copyrightText"), VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element) }; } private Spdx3SpdxDocument ParseSpdxDocument(JsonElement element, string spdxId) { return new Spdx3SpdxDocument { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = GetStringProperty(element, "name"), Summary = GetStringProperty(element, "summary"), Description = GetStringProperty(element, "description"), Comment = GetStringProperty(element, "comment"), Element = GetStringArrayProperty(element, "element"), RootElement = GetStringArrayProperty(element, "rootElement"), VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element) }; } private Spdx3Relationship ParseRelationship(JsonElement element, string spdxId) { var toValue = GetStringProperty(element, "to"); var toArray = toValue != null ? [toValue] : GetStringArrayProperty(element, "to"); var relationshipType = GetStringProperty(element, "relationshipType") ?? "Other"; if (!Enum.TryParse(relationshipType, ignoreCase: true, out var relType)) { relType = Spdx3RelationshipType.Other; } return new Spdx3Relationship { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), From = GetStringProperty(element, "from") ?? string.Empty, To = toArray, RelationshipType = relType, Comment = GetStringProperty(element, "comment"), VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element) }; } private Spdx3Build ParseBuild(JsonElement element, string spdxId) { // Parse timestamps DateTimeOffset? buildStartTime = null; DateTimeOffset? buildEndTime = null; var startTimeStr = GetStringProperty(element, "build_buildStartTime"); if (!string.IsNullOrEmpty(startTimeStr) && DateTimeOffset.TryParse(startTimeStr, out var parsedStart)) { buildStartTime = parsedStart; } var endTimeStr = GetStringProperty(element, "build_buildEndTime"); if (!string.IsNullOrEmpty(endTimeStr) && DateTimeOffset.TryParse(endTimeStr, out var parsedEnd)) { buildEndTime = parsedEnd; } // Parse config source digests var configSourceDigests = ImmutableArray.Empty; if (element.TryGetProperty("build_configSourceDigest", out var digestsElement) && digestsElement.ValueKind == JsonValueKind.Array) { var digests = new List(); foreach (var digestEl in digestsElement.EnumerateArray()) { if (digestEl.ValueKind == JsonValueKind.Object) { var algorithm = GetStringProperty(digestEl, "algorithm") ?? "sha256"; var hashValue = GetStringProperty(digestEl, "hashValue") ?? string.Empty; digests.Add(new Spdx3BuildHash { Algorithm = algorithm, HashValue = hashValue }); } } configSourceDigests = digests.ToImmutableArray(); } // Parse environment and parameters as dictionaries var environment = ParseDictionary(element, "build_environment"); var parameters = ParseDictionary(element, "build_parameter"); return new Spdx3Build { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = GetStringProperty(element, "name"), Summary = GetStringProperty(element, "summary"), Description = GetStringProperty(element, "description"), BuildType = GetStringProperty(element, "build_buildType") ?? string.Empty, BuildId = GetStringProperty(element, "build_buildId"), BuildStartTime = buildStartTime, BuildEndTime = buildEndTime, ConfigSourceUri = GetStringArrayProperty(element, "build_configSourceUri"), ConfigSourceDigest = configSourceDigests, ConfigSourceEntrypoint = GetStringArrayProperty(element, "build_configSourceEntrypoint"), Environment = environment, Parameter = parameters, VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element) }; } private static ImmutableDictionary ParseDictionary(JsonElement element, string propertyName) { if (!element.TryGetProperty(propertyName, out var dictElement) || dictElement.ValueKind != JsonValueKind.Object) { return ImmutableDictionary.Empty; } var dict = new Dictionary(StringComparer.Ordinal); foreach (var property in dictElement.EnumerateObject()) { if (property.Value.ValueKind == JsonValueKind.String) { dict[property.Name] = property.Value.GetString() ?? string.Empty; } } return dict.ToImmutableDictionary(); } private Spdx3Vulnerability ParseVulnerability(JsonElement element, string spdxId) { DateTimeOffset? publishedTime = null; DateTimeOffset? modifiedTime = null; DateTimeOffset? withdrawnTime = null; var publishedStr = GetStringProperty(element, "security_publishedTime"); if (!string.IsNullOrEmpty(publishedStr) && DateTimeOffset.TryParse(publishedStr, out var parsedPublished)) { publishedTime = parsedPublished; } var modifiedStr = GetStringProperty(element, "security_modifiedTime"); if (!string.IsNullOrEmpty(modifiedStr) && DateTimeOffset.TryParse(modifiedStr, out var parsedModified)) { modifiedTime = parsedModified; } var withdrawnStr = GetStringProperty(element, "security_withdrawnTime"); if (!string.IsNullOrEmpty(withdrawnStr) && DateTimeOffset.TryParse(withdrawnStr, out var parsedWithdrawn)) { withdrawnTime = parsedWithdrawn; } return new Spdx3Vulnerability { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = GetStringProperty(element, "name"), Summary = GetStringProperty(element, "summary"), Description = GetStringProperty(element, "description"), PublishedTime = publishedTime, ModifiedTime = modifiedTime, WithdrawnTime = withdrawnTime, ExternalRefs = ParseExternalRefs(element), ExternalIdentifiers = ParseExternalIdentifiers(element) }; } private Spdx3VulnAssessmentRelationship ParseVexAssessment( JsonElement element, string spdxId, string type) { var assessedElement = GetStringProperty(element, "security_assessedElement") ?? string.Empty; var from = GetStringProperty(element, "from") ?? string.Empty; var toValue = GetStringProperty(element, "to"); var toArray = toValue != null ? ImmutableArray.Create(toValue) : GetStringArrayProperty(element, "to"); DateTimeOffset? publishedTime = null; var publishedStr = GetStringProperty(element, "security_publishedTime"); if (!string.IsNullOrEmpty(publishedStr) && DateTimeOffset.TryParse(publishedStr, out var parsedPublished)) { publishedTime = parsedPublished; } DateTimeOffset? actionStatementTime = null; var actionTimeStr = GetStringProperty(element, "security_actionStatementTime"); if (!string.IsNullOrEmpty(actionTimeStr) && DateTimeOffset.TryParse(actionTimeStr, out var parsedActionTime)) { actionStatementTime = parsedActionTime; } var vexVersion = GetStringProperty(element, "security_vexVersion"); var statusNotes = GetStringProperty(element, "security_statusNotes"); var actionStatement = GetStringProperty(element, "security_actionStatement"); var impactStatement = GetStringProperty(element, "security_impactStatement"); var suppliedBy = GetStringProperty(element, "security_suppliedBy"); // Parse justification for not_affected Spdx3VexJustificationType? justificationType = null; var justificationStr = GetStringProperty(element, "security_justificationType"); if (!string.IsNullOrEmpty(justificationStr) && Enum.TryParse(justificationStr, ignoreCase: true, out var parsed)) { justificationType = parsed; } return type switch { "security_VexAffectedVulnAssessmentRelationship" => new Spdx3VexAffectedVulnAssessmentRelationship { SpdxId = spdxId, Type = type, AssessedElement = assessedElement, From = from, To = toArray, RelationshipType = Spdx3RelationshipType.Affects, VexVersion = vexVersion, StatusNotes = statusNotes, ActionStatement = actionStatement, ActionStatementTime = actionStatementTime, PublishedTime = publishedTime, SuppliedBy = suppliedBy }, "security_VexNotAffectedVulnAssessmentRelationship" => new Spdx3VexNotAffectedVulnAssessmentRelationship { SpdxId = spdxId, Type = type, AssessedElement = assessedElement, From = from, To = toArray, RelationshipType = Spdx3RelationshipType.DoesNotAffect, VexVersion = vexVersion, StatusNotes = statusNotes, ImpactStatement = impactStatement, JustificationType = justificationType, PublishedTime = publishedTime, SuppliedBy = suppliedBy }, "security_VexFixedVulnAssessmentRelationship" => new Spdx3VexFixedVulnAssessmentRelationship { SpdxId = spdxId, Type = type, AssessedElement = assessedElement, From = from, To = toArray, RelationshipType = Spdx3RelationshipType.FixedIn, VexVersion = vexVersion, StatusNotes = statusNotes, PublishedTime = publishedTime, SuppliedBy = suppliedBy }, "security_VexUnderInvestigationVulnAssessmentRelationship" => new Spdx3VexUnderInvestigationVulnAssessmentRelationship { SpdxId = spdxId, Type = type, AssessedElement = assessedElement, From = from, To = toArray, RelationshipType = Spdx3RelationshipType.UnderInvestigationFor, VexVersion = vexVersion, StatusNotes = statusNotes, PublishedTime = publishedTime, SuppliedBy = suppliedBy }, _ => throw new ArgumentException($"Unknown VEX assessment type: {type}") }; } private Spdx3CvssV3VulnAssessmentRelationship ParseCvssAssessment(JsonElement element, string spdxId) { var assessedElement = GetStringProperty(element, "security_assessedElement") ?? string.Empty; var from = GetStringProperty(element, "from") ?? string.Empty; var toValue = GetStringProperty(element, "to"); var toArray = toValue != null ? ImmutableArray.Create(toValue) : GetStringArrayProperty(element, "to"); decimal? baseScore = null; if (element.TryGetProperty("security_score", out var scoreEl) && scoreEl.TryGetDecimal(out var score)) { baseScore = score; } var vectorString = GetStringProperty(element, "security_vectorString"); // Parse severity enum Spdx3CvssSeverity? severityEnum = null; var severityStr = GetStringProperty(element, "security_severity"); if (!string.IsNullOrEmpty(severityStr) && Enum.TryParse(severityStr, ignoreCase: true, out var parsedSeverity)) { severityEnum = parsedSeverity; } return new Spdx3CvssV3VulnAssessmentRelationship { SpdxId = spdxId, Type = "security_CvssV3VulnAssessmentRelationship", AssessedElement = assessedElement, From = from, To = toArray, RelationshipType = Spdx3RelationshipType.HasAssessmentFor, Score = baseScore, VectorString = vectorString, Severity = severityEnum }; } private Spdx3EpssVulnAssessmentRelationship ParseEpssAssessment(JsonElement element, string spdxId) { var assessedElement = GetStringProperty(element, "security_assessedElement") ?? string.Empty; var from = GetStringProperty(element, "from") ?? string.Empty; var toValue = GetStringProperty(element, "to"); var toArray = toValue != null ? ImmutableArray.Create(toValue) : GetStringArrayProperty(element, "to"); decimal? probability = null; if (element.TryGetProperty("security_probability", out var probEl) && probEl.TryGetDecimal(out var prob)) { probability = prob; } decimal? percentile = null; if (element.TryGetProperty("security_percentile", out var percEl) && percEl.TryGetDecimal(out var perc)) { percentile = perc; } return new Spdx3EpssVulnAssessmentRelationship { SpdxId = spdxId, Type = "security_EpssVulnAssessmentRelationship", AssessedElement = assessedElement, From = from, To = toArray, RelationshipType = Spdx3RelationshipType.HasAssessmentFor, Probability = probability, Percentile = percentile }; } private T ParseAgent(JsonElement element, string spdxId) where T : Spdx3Element { var name = GetStringProperty(element, "name") ?? string.Empty; if (typeof(T) == typeof(Spdx3Person)) { return (T)(object)new Spdx3Person { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = name }; } if (typeof(T) == typeof(Spdx3Organization)) { return (T)(object)new Spdx3Organization { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = name }; } if (typeof(T) == typeof(Spdx3Tool)) { return (T)(object)new Spdx3Tool { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = name }; } throw new InvalidOperationException($"Unsupported agent type: {typeof(T)}"); } private Spdx3Element? ParseGenericElement( JsonElement element, string spdxId, string type, List warnings) { // For unknown types, we could create a generic element // For now, log a warning and skip warnings.Add(new Spdx3ParseWarning("UNKNOWN_TYPE", $"Unknown element type: {type}", spdxId)); return null; } private Spdx3CreationInfo? ExtractCreationInfo( JsonElement element, List errors) { if (!element.TryGetProperty("creationInfo", out var ciElement)) { return null; } if (ciElement.ValueKind == JsonValueKind.String) { // Reference to CreationInfo - will be resolved later return null; } if (ciElement.ValueKind != JsonValueKind.Object) { return null; } var specVersion = GetStringProperty(ciElement, "specVersion"); if (string.IsNullOrEmpty(specVersion)) { return null; } var createdStr = GetStringProperty(ciElement, "created"); if (!DateTimeOffset.TryParse(createdStr, out var created)) { created = _timeProvider.GetUtcNow(); } var profileStrings = GetStringArrayProperty(ciElement, "profile"); var profiles = profileStrings .Select(p => Spdx3ProfileUris.Parse(p)) .Where(p => p.HasValue) .Select(p => p!.Value) .ToImmutableArray(); return new Spdx3CreationInfo { Id = GetStringProperty(ciElement, "@id"), Type = GetStringProperty(ciElement, "@type"), SpecVersion = specVersion, Created = created, CreatedBy = GetStringArrayProperty(ciElement, "createdBy"), CreatedUsing = GetStringArrayProperty(ciElement, "createdUsing"), Profile = profiles, DataLicense = GetStringProperty(ciElement, "dataLicense"), Comment = GetStringProperty(ciElement, "comment") }; } private ImmutableArray ParseIntegrityMethods(JsonElement element) { if (!element.TryGetProperty("verifiedUsing", out var verifiedUsing)) { return []; } if (verifiedUsing.ValueKind != JsonValueKind.Array) { return []; } var methods = new List(); foreach (var item in verifiedUsing.EnumerateArray()) { var algorithm = GetStringProperty(item, "algorithm"); var hashValue = GetStringProperty(item, "hashValue"); if (!string.IsNullOrEmpty(algorithm) && !string.IsNullOrEmpty(hashValue)) { if (Enum.TryParse(algorithm.Replace("-", "_"), ignoreCase: true, out var algo)) { methods.Add(new Spdx3Hash { Type = GetStringProperty(item, "@type"), Algorithm = algo, HashValue = hashValue.ToLowerInvariant(), Comment = GetStringProperty(item, "comment") }); } } } return [.. methods]; } private ImmutableArray ParseExternalRefs(JsonElement element) { if (!element.TryGetProperty("externalRef", out var externalRef)) { return []; } if (externalRef.ValueKind != JsonValueKind.Array) { return []; } var refs = new List(); foreach (var item in externalRef.EnumerateArray()) { refs.Add(new Spdx3ExternalRef { Type = GetStringProperty(item, "@type"), Locator = GetStringArrayProperty(item, "locator"), ContentType = GetStringProperty(item, "contentType"), Comment = GetStringProperty(item, "comment") }); } return [.. refs]; } private ImmutableArray ParseExternalIdentifiers(JsonElement element) { if (!element.TryGetProperty("externalIdentifier", out var externalId)) { return []; } if (externalId.ValueKind != JsonValueKind.Array) { return []; } var identifiers = new List(); foreach (var item in externalId.EnumerateArray()) { var identifier = GetStringProperty(item, "identifier"); if (string.IsNullOrEmpty(identifier)) { continue; } var typeStr = GetStringProperty(item, "externalIdentifierType"); Spdx3ExternalIdentifierType? idType = null; if (!string.IsNullOrEmpty(typeStr) && Enum.TryParse(typeStr, ignoreCase: true, out var parsed)) { idType = parsed; } identifiers.Add(new Spdx3ExternalIdentifier { Type = GetStringProperty(item, "@type"), ExternalIdentifierType = idType, Identifier = identifier, Comment = GetStringProperty(item, "comment"), IssuingAuthority = GetStringProperty(item, "issuingAuthority") }); } return [.. identifiers]; } private static string? GetStringProperty(JsonElement element, string propertyName) { if (element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String) { return prop.GetString(); } return null; } private static ImmutableArray GetStringArrayProperty(JsonElement element, string propertyName) { if (!element.TryGetProperty(propertyName, out var prop)) { return []; } if (prop.ValueKind == JsonValueKind.String) { var value = prop.GetString(); return value != null ? [value] : []; } if (prop.ValueKind == JsonValueKind.Array) { var list = new List(); foreach (var item in prop.EnumerateArray()) { if (item.ValueKind == JsonValueKind.String) { var value = item.GetString(); if (value != null) { list.Add(value); } } } return [.. list]; } return []; } private static string? GetContextReference(JsonElement contextElement) { if (contextElement.ValueKind == JsonValueKind.String) { return contextElement.GetString(); } if (contextElement.ValueKind == JsonValueKind.Array) { foreach (var item in contextElement.EnumerateArray()) { if (item.ValueKind == JsonValueKind.String) { return item.GetString(); } } } return null; } }