CycloneDX / CycloneDX/cyclonedx-cli

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

Open
#498 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
C#
Stars
541
Forks
82
PR merge metrics
No merged PRs in 30d

Description

### 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` for some enum types, such as:
```
HyphenToUnderscoreEnumConverter
HyphenToUnderscoreEnumConverter
```

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());
```

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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.