Skip to content

Commit 0e58ff2

Browse files
jdsikarmessaou
authored andcommitted
feat(jsonschemagen): add --include-null/--no-include-null CLI option
Expose the existing JsonSchemaGenerator.include_null field on the gen-json-schema CLI. --no-include-null forbids explicit JSON null in optional slots so optionality is expressed only via absence from required (JSON Schema Validation 6.5.3), keeping the bare value type (6.1.1) -- needed for strict parity with reference schemas that forbid null. Default unchanged (include_null=True). Tested at the CLI surface via CliRunner over scalar, multivalued, and required slots; the standards rationale lives in the include_null field docstring. Signed-off-by: Carlo van Driesten <carlo.van-driesten@bmw.de>
1 parent 7414ab9 commit 0e58ff2

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

packages/linkml/src/linkml/generators/jsonschemagen.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,18 @@ class JsonSchemaGenerator(Generator, LifecycleMixin):
423423
top_level_schema: JsonSchema = None
424424

425425
include_null: bool = True
426-
"""Whether to include a "null" type in optional slots"""
426+
"""Whether optional (non-required) slots also accept an explicit JSON ``null``.
427+
428+
When ``True`` (default) an optional slot is rendered with ``null`` added to its
429+
type (e.g. ``["string", "null"]``), so an explicit ``null`` value validates. When
430+
``False`` the slot keeps its bare type and optionality is expressed solely by
431+
absence from ``required``.
432+
433+
JSON Schema treats presence (``required``, Validation 6.5.3) as separate from the
434+
value type (``type``, Validation 6.1.1, where ``null`` is one of the value types),
435+
and JSON ``null`` is a distinct value, not an absent member (RFC 8259 sec. 3). Set
436+
this ``False`` for strict parity with reference schemas that declare a bare type
437+
and forbid ``null``."""
427438

428439
preserve_names: bool = False
429440
"""If true, preserve LinkML element names in JSON Schema output (e.g., for $defs, properties, $ref targets)."""
@@ -1064,6 +1075,13 @@ def serialize(self, **kwargs) -> str:
10641075
show_default=True,
10651076
help="If set, expand subproperty_of constraints to enum constraints.",
10661077
)
1078+
@click.option(
1079+
"--include-null/--no-include-null",
1080+
default=True,
1081+
show_default=True,
1082+
help="If set (default), optional slots also accept an explicit JSON null. "
1083+
"Use --no-include-null to forbid explicit null on optional slots.",
1084+
)
10671085
@click.version_option(__version__, "-V", "--version")
10681086
def cli(yamlfile, **kwargs):
10691087
"""Generate JSON Schema representation of a LinkML model"""

tests/linkml/test_scripts/test_gen_json_schema.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import re
23

34
import pytest
@@ -70,3 +71,49 @@ def test_include_option(input_path):
7071
extra_import_path = str(input_path("deprecation.yaml"))
7172
result = runner.invoke(cli, ["--include", extra_import_path, schema])
7273
assert "C4" in result.output
74+
75+
76+
_INCLUDE_NULL_SCHEMA = """
77+
id: https://example.org/test-include-null-cli
78+
name: test-include-null-cli
79+
prefixes:
80+
linkml: https://w3id.org/linkml/
81+
default_range: string
82+
imports:
83+
- linkml:types
84+
classes:
85+
C:
86+
attributes:
87+
opt:
88+
range: string
89+
opt_multi:
90+
range: string
91+
multivalued: true
92+
req:
93+
range: string
94+
required: true
95+
"""
96+
97+
98+
@pytest.mark.parametrize(
99+
"flag,expected_opt,expected_opt_multi",
100+
[
101+
([], ["string", "null"], ["array", "null"]),
102+
(["--include-null"], ["string", "null"], ["array", "null"]),
103+
(["--no-include-null"], "string", "array"),
104+
],
105+
)
106+
def test_include_null_cli_option(flag, expected_opt, expected_opt_multi, tmp_path):
107+
"""The --include-null/--no-include-null CLI option must control whether optional
108+
slots accept an explicit JSON null, across scalar and multivalued ranges, while
109+
leaving required slots unaffected."""
110+
schema = tmp_path / "schema.yaml"
111+
schema.write_text(_INCLUDE_NULL_SCHEMA)
112+
runner = CliRunner()
113+
result = runner.invoke(cli, flag + [str(schema)])
114+
assert result.exit_code == 0, result.output
115+
props = json.loads(result.output)["$defs"]["C"]["properties"]
116+
assert props["opt"]["type"] == expected_opt
117+
assert props["opt_multi"]["type"] == expected_opt_multi
118+
# required scalar slot is unaffected by the flag
119+
assert props["req"]["type"] == "string"

0 commit comments

Comments
 (0)