Skip to content

SPDX 2.3 conversion fails for valid primaryPackagePurpose: "OPERATING-SYSTEM" #498

Description

@schmidHolger

Description

CycloneDX CLI fails to convert a valid SPDX 2.3 JSON document when a package contains the following field:
"primaryPackagePurpose": "OPERATING-SYSTEM"

OPERATING-SYSTEM is the valid value defined by the SPDX 2.3 specification.

The CLI throws an unhandled System.Text.Json.JsonException while deserializing the SPDX document.

Replacing the valid SPDX value: OPERATING-SYSTEM with the non-standard value: OPERATING_SYSTEM allows the conversion to proceed.

Environment

CycloneDX CLI version: 0.32.0
Platform: Linux x64 / WSL Ubuntu 24.04
Input format: SPDX JSON 2.3
Output format: CycloneDX JSON
Requested output version: CycloneDX 1.6

Command

./cyclonedx-linux-x64 convert \
  --input-format spdxjson \
  --output-format json \
  --output-version v1_6 \
  --input-file minimal.spdx.json \
  --output-file output.cdx.json

Minimal reproducer

{
  "spdxVersion": "SPDX-2.3",
  "dataLicense": "CC0-1.0",
  "SPDXID": "SPDXRef-DOCUMENT",
  "name": "operating-system-test",
  "documentNamespace": "https://example.com/spdx/operating-system-test",
  "creationInfo": {
    "created": "2026-07-14T08:00:00Z",
    "creators": [
      "Tool: minimal-reproducer"
    ]
  },
  "packages": [
    {
      "name": "Example Linux",
      "SPDXID": "SPDXRef-OperatingSystem",
      "versionInfo": "1.0",
      "downloadLocation": "NONE",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "primaryPackagePurpose": "OPERATING-SYSTEM"
    }
  ],
  "relationships": [
    {
      "spdxElementId": "SPDXRef-DOCUMENT",
      "relationshipType": "DESCRIBES",
      "relatedSpdxElement": "SPDXRef-OperatingSystem"
    }
  ]
}

Actual result

The conversion terminates with the following exception:

Unhandled exception: System.Text.Json.JsonException: The JSON value could not be converted to System.Nullable`1[CycloneDX.Spdx.Models.v2_3.PrimaryPackagePurposeType]. Path: $.packages[0].primaryPackagePurpose
   at System.Text.Json.ThrowHelper.ThrowJsonException(String message)
   at System.Text.Json.Serialization.Converters.EnumConverter`1.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
   at System.Text.Json.Serialization.Converters.NullableConverter`1.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
   at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, T& value, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.ContinueDeserialize[TReadBufferState,TStream](TReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack, T& value)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsync[TReadBufferState,TStream](TStream utf8Json, TReadBufferState bufferState, CancellationToken cancellationToken)
   at CycloneDX.Spdx.Serialization.JsonSerializer.DeserializeAsync(Stream jsonStream)
   at CycloneDX.Cli.CliUtils.InputBomHelper(String filename, ConvertFormat format)
   at CycloneDX.Cli.Commands.ConvertCommand.Convert(ConvertCommandOptions options)

Expected result

The CLI should successfully deserialize the SPDX 2.3 document and convert the package to a CycloneDX component.

Analysis

The SPDX model defines the C# enum member as:

OPERATING_SYSTEM

while the SPDX 2.3 JSON representation requires:

OPERATING-SYSTEM

The SPDX JSON serializer currently registers HyphenToUnderscoreEnumConverter<T> for some enum types, such as:

HyphenToUnderscoreEnumConverter<ExternalRefCategory>
HyphenToUnderscoreEnumConverter<ChecksumAlgorithm>

However, no equivalent converter appears to be registered for:

PrimaryPackagePurposeType

The generic JsonStringEnumConverter therefore attempts to match OPERATING-SYSTEM directly against the C# enum member OPERATING_SYSTEM and fails.

The issue likely originates in CycloneDX.Spdx.Serialization.JsonSerializer from the cyclonedx-dotnet-library, rather than in the CLI conversion logic itself.

Possible fix

Register the existing converter for PrimaryPackagePurposeType before the generic JsonStringEnumConverter:

options.Converters.Add(
    new HyphenToUnderscoreEnumConverter<PrimaryPackagePurposeType>());

A regression test should verify both directions:

Deserializing:

"primaryPackagePurpose": "OPERATING-SYSTEM"

into:

PrimaryPackagePurposeType.OPERATING_SYSTEM

Serializing the enum back to the SPDX-compliant value:
"primaryPackagePurpose": "OPERATING-SYSTEM"
Workaround

As a temporary workaround, replacing:

"primaryPackagePurpose": "OPERATING-SYSTEM"

with:

"primaryPackagePurpose": "OPERATING_SYSTEM"

allows the conversion to complete, but the resulting temporary SPDX input is not compliant with the SPDX 2.3 specification.

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