Skip to content

Nullability on enums broken #445

Open

Description

@Falco20019

I tried to create an VEX for a SBOM document. The analysis is In_Triage and therefore no justification is given right now. According to the 1.6 specification, this is fine.

var bom = new Bom
{
    SpecVersion = SpecificationVersion.v1_6,
    SerialNumber = $"urn:uuid:{Guid.NewGuid()}",
    Version = 1,
    Vulnerabilities =
    [
        new()
        {
            Analysis = new()
            {
                State = ImpactAnalysisState.In_Triage
            }
        }
    ]
};

When serializing it to JSON using CycloneDX.Json.Serializer.SerializeAsync I will get:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.6",
  "serialNumber": "urn:uuid:be8f9457-aaf8-48ae-8f9e-b914f4b7cee1",
  "version": 1,
  "vulnerabilities": [
    {
      "analysis": {
        "state": "in_triage",
        "justification": "null"
      }
    }
  ]
}

cyclonedx-cli validate --input-file <file> will show:

Validating JSON BOM...
Validation failed:
Some properties did not match the required schema
http://cyclonedx.org/schema/bom-1.6.schema.json#
On instance: :
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.6",
  "serialNumber": "urn:uuid:be8f9457-aaf8-48ae-8f9e-b914f4b7cee1",
  "version": 1,
  "vulnerabilities": [
    {
      "analysis": {
        "state": "in_triage",
        "justification": "null"
      }
    }
  ]
}
Some items do not match the required schema; failing indexes: [0]
http://cyclonedx.org/schema/bom-1.6.schema.json#/properties/vulnerabilities
On instance: /vulnerabilities:
[
    {
      "analysis": {
        "state": "in_triage",
        "justification": "null"
      }
    }
  ]
Some properties did not match the required schema
http://cyclonedx.org/schema/bom-1.6.schema.json#/definitions/vulnerability
On instance: /vulnerabilities/0:
{
      "analysis": {
        "state": "in_triage",
        "justification": "null"
      }
    }
Some properties did not match the required schema
http://cyclonedx.org/schema/bom-1.6.schema.json#/definitions/vulnerability/properties/analysis
On instance: /vulnerabilities/0/analysis:
{
        "state": "in_triage",
        "justification": "null"
      }
Value should match one of the values specified by the enum
http://cyclonedx.org/schema/bom-1.6.schema.json#/definitions/impactAnalysisJustification
On instance: /vulnerabilities/0/analysis/justification:
null
BOM is not valid.

Manually removing the justification line results in

Validating JSON BOM...
BOM validated successfully.

I therefore assume, that any enum with Null as protobuf helper should not be serialized at all. Can you please fix all types?

Activity

  1. Falco20019 commented on Sep 15, 2026

    @Falco20019
    Author

    As workaround, I now use the following helper class:

    using System.Text.Json;
    using System.Text.Json.Nodes;
    using System.Text.Json.Serialization;
    using Analysis = CycloneDX.Models.Vulnerabilities.Analysis;
    
    namespace <YourNamespace>;
    
    /// <inheritdoc />
    public class AnalysisConverter : JsonConverter<Analysis>
    {
        /// <inheritdoc />
        public override Analysis? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var defaultOptions = new JsonSerializerOptions(options);
            defaultOptions.Converters.Remove(this);
            return JsonSerializer.Deserialize<Analysis>(ref reader, defaultOptions);
        }
    
        /// <inheritdoc />
        public override void Write(Utf8JsonWriter writer, Analysis? value, JsonSerializerOptions options)
        {
            var defaultOptions = new JsonSerializerOptions(options);
            defaultOptions.Converters.Remove(this);
    
            var json = JsonSerializer.SerializeToNode(value, defaultOptions);
            if (json is not JsonObject jsonObject)
            {
                json?.WriteTo(writer);
                return;
            }
    
            var propertiesToRemove = jsonObject
                .Where(property =>
                    property.Key.Equals(nameof(Analysis.State), StringComparison.OrdinalIgnoreCase) ||
                    property.Key.Equals(nameof(Analysis.Justification), StringComparison.OrdinalIgnoreCase))
                .Where(property =>
                    property.Value is null ||
                    (property.Value is JsonValue jsonValue &&
                     jsonValue.TryGetValue<string>(out var enumValue) &&
                     enumValue.Equals("Null", StringComparison.OrdinalIgnoreCase)))
                .Select(property => property.Key)
                .ToArray();
    
            foreach (var propertyKey in propertiesToRemove)
            {
                jsonObject.Remove(propertyKey);
            }
    
            jsonObject.WriteTo(writer);
        }
    }

    And on serialization I use:

    // TODO: Currently broken for null, using workaround: https://github.com/CycloneDX/cyclonedx-dotnet-library/issues/445
    //await CycloneDX.Json.Serializer.SerializeAsync(sbom, outputStream).ConfigureAwait(ConfigureAwaitOptions.None);
    var options = Utils.GetJsonSerializerOptions();
    options.Converters.Insert(0, new AnalysisConverter());
    // Workaround since BomUtils.GetBomForSerialization is not public...
    var method = typeof(BomUtils).GetMethod("GetBomForSerialization", BindingFlags.Static | BindingFlags.NonPublic);
    sbom = (Bom)method!.Invoke(null, [sbom])!;
    await JsonSerializer.SerializeAsync(outputStream, sbom, options).ConfigureAwait(ConfigureAwaitOptions.None);
  2. andreas-hilti commented on Sep 16, 2026

    @andreas-hilti
    Contributor

    I think the fix would be along the line (in Analysis):
    public bool ShouldSerializeJustification() { return Justification != ImpactAnalysisJustification.Null; }

    but this unfortunately this will only help on xml, not on json.

    Maybe this would do:

            [XmlElement("justification")]
            [JsonIgnore]
            [ProtoMember(2)]
            public ImpactAnalysisJustification Justification { get; set; }
            public bool ShouldSerializeJustification() { return Justification != ImpactAnalysisJustification.Null; }
    
            [XmlIgnore]
            [JsonPropertyName("justification")]
            public ImpactAnalysisJustification? NullableJustification
            {
                get
                {
                    return Justification != ImpactAnalysisJustification.Null ? (ImpactAnalysisJustification?)Justification : null;
                }
                set
                {
                    Justification = (value != null) ? (ImpactAnalysisJustification)value : ImpactAnalysisJustification.Null;
                }
            }
    
  3. andreas-hilti commented on Sep 16, 2026

    @andreas-hilti
    Contributor

    actually probably simpler:

            [XmlElement("justification")]
            [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
            [ProtoMember(2)]
            public ImpactAnalysisJustification Justification { get; set; }
            public bool ShouldSerializeJustification() { return Justification != ImpactAnalysisJustification.Null; }
    
  4. Falco20019 commented on Sep 17, 2026

    @Falco20019
    Author

    Sounds right with the JsonIgnore. Make sure that there are also other enums like that that might be optional and have the Null entry.

  5. andreas-hilti commented on Sep 20, 2026

    @andreas-hilti
    Contributor

    See #453

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions