From acc2df3812e23345e43a0c0c6a419f179ef17d28 Mon Sep 17 00:00:00 2001 From: jdsika Date: Thu, 2 Apr 2026 17:03:54 +0200 Subject: [PATCH] fix(generators): add --xsd-anyuri-as-iri flag for cross-generator IRI consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSON-LD processors treat xsd:anyURI as an opaque string literal, so range:uri/uriorcurie slots get xsd:anyURI coercion instead of proper IRI node semantics (@type:@id, owl:ObjectProperty, sh:IRI). Add an opt-in --xsd-anyuri-as-iri flag that promotes xsd:anyURI ranges to IRI semantics across all three generators: - JSON-LD context: @type: xsd:anyURI → @type: @id - OWL: DatatypeProperty → ObjectProperty (no rdfs:range restriction) - SHACL: sh:datatype xsd:anyURI → sh:nodeKind sh:IRI The flag only affects types whose XSD mapping is xsd:anyURI (uri and uriorcurie). The curie type (xsd:string) is correctly excluded via is_xsd_anyuri_range() to maintain cross-generator consistency. Standards basis: - OWL 2 §5.3-5.4 (ObjectProperty vs DatatypeProperty) - SHACL §4.8.1 (sh:nodeKind sh:IRI) - JSON-LD 1.1 §4.2.2 (type coercion with @id) - RDF 1.1 §3.2-3.3 (IRIs as first-class nodes, not string literals) Signed-off-by: jdsika --- .../linkml/generators/common/subproperty.py | 33 ++ .../src/linkml/generators/jsonldcontextgen.py | 22 +- .../linkml/src/linkml/generators/owlgen.py | 56 ++- .../test_generators/test_jsonldcontextgen.py | 372 ++++++++++++++++++ 4 files changed, 472 insertions(+), 11 deletions(-) diff --git a/packages/linkml/src/linkml/generators/common/subproperty.py b/packages/linkml/src/linkml/generators/common/subproperty.py index 4687c3821..9b136e242 100644 --- a/packages/linkml/src/linkml/generators/common/subproperty.py +++ b/packages/linkml/src/linkml/generators/common/subproperty.py @@ -15,6 +15,10 @@ CURIE_TYPES: frozenset[str] = frozenset({"uriorcurie", "curie"}) URI_TYPES: frozenset[str] = frozenset({"uri"}) +# Types whose XSD mapping is xsd:anyURI (not xsd:string). +# ``curie`` maps to xsd:string and is deliberately excluded. +_ANYURI_TYPES: frozenset[str] = frozenset({"uri", "uriorcurie"}) + def is_uri_range(sv: SchemaView, range_type: str | None) -> bool: """ @@ -63,6 +67,35 @@ def is_curie_range(sv: SchemaView, range_type: str | None) -> bool: return False +def is_xsd_anyuri_range(sv: SchemaView, range_type: str | None) -> bool: + """Check if range type resolves to ``xsd:anyURI``. + + Returns True for ``uri``, ``uriorcurie``, and types that inherit from them. + Returns False for ``curie`` (which maps to ``xsd:string``). + + This is the correct predicate for the ``--xsd-anyuri-as-iri`` flag: only + types whose XSD representation is ``xsd:anyURI`` should be promoted from + literal to IRI semantics. ``curie`` is a compact string representation + that resolves to ``xsd:string`` and must not be affected. + + :param sv: SchemaView for type ancestry lookup + :param range_type: The range type to check + :return: True if range type maps to xsd:anyURI + """ + if range_type is None: + return False + + if range_type in _ANYURI_TYPES: + return True + + if range_type in sv.all_types(): + type_ancestors = set(sv.type_ancestors(range_type)) + if type_ancestors & _ANYURI_TYPES: + return True + + return False + + def format_slot_value_for_range(sv: SchemaView, slot_name: str, range_type: str | None) -> str: """ Format slot value according to the declared range type. diff --git a/packages/linkml/src/linkml/generators/jsonldcontextgen.py b/packages/linkml/src/linkml/generators/jsonldcontextgen.py index 60eaa9ffd..a7fab03a9 100644 --- a/packages/linkml/src/linkml/generators/jsonldcontextgen.py +++ b/packages/linkml/src/linkml/generators/jsonldcontextgen.py @@ -23,6 +23,10 @@ URI_RANGES = (SHEX.nonliteral, SHEX.bnode, SHEX.iri) +# Extended URI_RANGES that also treats xsd:anyURI as an IRI reference (@id) +# rather than a typed literal. Opt-in via --xsd-anyuri-as-iri flag. +URI_RANGES_WITH_XSD = (*URI_RANGES, XSD.anyURI) + ENUM_CONTEXT = { "text": "skos:notation", "description": "skos:prefLabel", @@ -58,6 +62,12 @@ class ContextGenerator(Generator): """If True, elements from imported schemas won't be included in the generated context""" _local_classes: set | None = field(default=None, repr=False) _local_slots: set | None = field(default=None, repr=False) + xsd_anyuri_as_iri: bool = False + """Map xsd:anyURI-typed ranges (uri, uriorcurie) to ``@type: @id`` instead of ``@type: xsd:anyURI``. + + This aligns the JSON-LD context with the SHACL generator, which emits + ``sh:nodeKind sh:IRI`` for the same types. + """ # Framing (opt-in via CLI flag) emit_frame: bool = False @@ -224,6 +234,7 @@ def _literal_coercion_for_ranges(self, ranges: list[str]) -> tuple[bool, str | N and "could not resolve safely because the branches disagree". """ coercions: set[str | None] = set() + uri_ranges = URI_RANGES_WITH_XSD if self.xsd_anyuri_as_iri else URI_RANGES for range_name in ranges: if range_name not in self.schema.types: continue @@ -232,7 +243,7 @@ def _literal_coercion_for_ranges(self, ranges: list[str]) -> tuple[bool, str | N range_uri = self.namespaces.uri_for(range_type.uri) if range_uri == XSD.string: coercions.add(None) - elif range_uri in URI_RANGES: + elif range_uri in uri_ranges: coercions.add("@id") else: coercions.add(range_type.uri) @@ -275,9 +286,10 @@ def visit_slot(self, aliased_slot_name: str, slot: SlotDefinition) -> None: self.emit_prefixes.add(skos) else: range_type = self.schema.types[slot.range] + uri_ranges = URI_RANGES_WITH_XSD if self.xsd_anyuri_as_iri else URI_RANGES if self.namespaces.uri_for(range_type.uri) == XSD.string: pass - elif self.namespaces.uri_for(range_type.uri) in URI_RANGES: + elif self.namespaces.uri_for(range_type.uri) in uri_ranges: slot_def["@type"] = "@id" else: slot_def["@type"] = range_type.uri @@ -390,6 +402,12 @@ def serialize( help="Use --exclude-imports to exclude imported elements from the generated JSON-LD context. This is useful when " "extending an ontology whose terms already have context definitions in their own JSON-LD context file.", ) +@click.option( + "--xsd-anyuri-as-iri/--no-xsd-anyuri-as-iri", + default=False, + show_default=True, + help="Map xsd:anyURI-typed ranges (uri, uriorcurie) to @type: @id instead of @type: xsd:anyURI.", +) @click.version_option(__version__, "-V", "--version") def cli(yamlfile, emit_frame, embed_context_in_frame, output, **args): """Generate jsonld @context definition from LinkML model""" diff --git a/packages/linkml/src/linkml/generators/owlgen.py b/packages/linkml/src/linkml/generators/owlgen.py index 33c58b0ec..7dcbebf38 100644 --- a/packages/linkml/src/linkml/generators/owlgen.py +++ b/packages/linkml/src/linkml/generators/owlgen.py @@ -19,6 +19,7 @@ from linkml import METAMODEL_NAMESPACE_NAME from linkml._version import __version__ +from linkml.generators.common.subproperty import is_xsd_anyuri_range from linkml.utils.deprecation import deprecation_warning from linkml.utils.generator import Generator, shared_arguments from linkml_runtime import SchemaView @@ -203,6 +204,24 @@ class OwlSchemaGenerator(Generator): constraint that every instance of the abstract class must also be an instance of one of its direct subclasses.""" + xsd_anyuri_as_iri: bool = False + """Treat ``range: uri`` / ``range: uriorcurie`` slots as ``owl:ObjectProperty`` + instead of ``owl:DatatypeProperty`` with ``rdfs:range xsd:anyURI``. + + This aligns the OWL output with the SHACL generator (which emits + ``sh:nodeKind sh:IRI``) and the JSON-LD context generator (which emits + ``@type: @id`` when its own ``--xsd-anyuri-as-iri`` flag is set). + + Without this flag, ``range: uri`` produces a semantic inconsistency: + OWL says the value is a literal (``DatatypeProperty``), while SHACL and + JSON-LD say it is an IRI node. Enabling the flag makes all three + generators consistent. + + When enabled, URI-range slots: + - become ``owl:ObjectProperty`` (not ``owl:DatatypeProperty``) + - have no ``rdfs:range`` restriction (any IRI is valid) + """ + def as_graph(self) -> Graph: """ Generate an rdflib Graph from the LinkML schema. @@ -746,14 +765,19 @@ def transform_class_slot_expression( this_owl_types = set() if range: if range in sv.all_types(imports=True): - self.slot_is_literal_map[main_slot.name].add(True) - this_owl_types.add(RDFS.Literal) - typ = sv.get_type(range) - if self.type_objects: - # TODO - owl_exprs.append(self._type_uri(typ.name)) + if self.xsd_anyuri_as_iri and is_xsd_anyuri_range(sv, range): + # xsd:anyURI ranges become ObjectProperty with no rdfs:range + self.slot_is_literal_map[main_slot.name].add(False) + this_owl_types.add(OWL.Thing) else: - owl_exprs.append(self._type_uri(typ.name)) + self.slot_is_literal_map[main_slot.name].add(True) + this_owl_types.add(RDFS.Literal) + typ = sv.get_type(range) + if self.type_objects: + # TODO + owl_exprs.append(self._type_uri(typ.name)) + else: + owl_exprs.append(self._type_uri(typ.name)) elif range in sv.all_enums(imports=True): # TODO: enums fill this in owl_exprs.append(self._enum_uri(EnumDefinitionName(range))) @@ -1330,8 +1354,9 @@ def _boolean_expression( def _range_is_datatype(self, slot: SlotDefinition) -> bool: if self.type_objects: return False - else: - return slot.range in self.schema.types + if self.xsd_anyuri_as_iri and is_xsd_anyuri_range(self.schemaview, slot.range): + return False + return slot.range in self.schema.types def _range_uri(self, slot: SlotDefinition) -> URIRef: if slot.range in self.schema.types: @@ -1450,6 +1475,8 @@ def slot_owl_type(self, slot: SlotDefinition) -> URIRef: elif range in sv.all_enums(): return OWL.ObjectProperty elif range in sv.all_types(): + if self.xsd_anyuri_as_iri and is_xsd_anyuri_range(sv, range): + return OWL.ObjectProperty return OWL.DatatypeProperty else: raise Exception(f"Unknown range: {slot.range}") @@ -1572,6 +1599,17 @@ def slot_owl_type(self, slot: SlotDefinition) -> URIRef: "By default such axioms are emitted for every abstract class that has direct is_a children." ), ) +@click.option( + "--xsd-anyuri-as-iri/--no-xsd-anyuri-as-iri", + default=False, + show_default=True, + help=( + "Treat range: uri / range: uriorcurie slots as owl:ObjectProperty (IRI node) " + "instead of owl:DatatypeProperty with rdfs:range xsd:anyURI (literal). " + "Aligns OWL output with the SHACL generator (sh:nodeKind sh:IRI) and " + "the JSON-LD context generator (--xsd-anyuri-as-iri → @type: @id)." + ), +) @click.version_option(__version__, "-V", "--version") def cli(yamlfile, metadata_profile: str, **kwargs): """Generate an OWL representation of a LinkML model diff --git a/tests/linkml/test_generators/test_jsonldcontextgen.py b/tests/linkml/test_generators/test_jsonldcontextgen.py index 6de23347a..21ba26beb 100644 --- a/tests/linkml/test_generators/test_jsonldcontextgen.py +++ b/tests/linkml/test_generators/test_jsonldcontextgen.py @@ -571,3 +571,375 @@ def test_exclude_imports(input_path): # Imported class and slot must NOT be present assert "BaseClass" not in ctx, "Imported class 'BaseClass' must not appear in exclude-imports context" assert "baseProperty" not in ctx, "Imported slot 'baseProperty' must not appear in exclude-imports context" + + +def test_xsd_anyuri_as_iri_flag(): + """Test that --xsd-anyuri-as-iri maps uri ranges to @type: @id. + + By default, ``range: uri`` (type_uri ``xsd:anyURI``) produces + ``@type: xsd:anyURI`` (typed literal). With ``xsd_anyuri_as_iri=True``, + it produces ``@type: @id`` (IRI node reference), aligning the JSON-LD + context with the SHACL generator which already emits ``sh:nodeKind sh:IRI`` + for the same type. + + See: + - W3C SHACL §4.8.1 sh:nodeKind (https://www.w3.org/TR/shacl/#NodeKindConstraintComponent) + - JSON-LD 1.1 §4.2.2 Type Coercion (https://www.w3.org/TR/json-ld11/#type-coercion) + - RDF 1.1 §3.3 Literals vs §3.2 IRIs (https://www.w3.org/TR/rdf11-concepts/) + """ + schema_yaml = """ +id: https://example.org/test-uri-context +name: test_uri_context + +prefixes: + ex: https://example.org/ + linkml: https://w3id.org/linkml/ + +imports: + - linkml:types + +default_prefix: ex +default_range: string + +slots: + homepage: + range: uri + slot_uri: ex:homepage + node_ref: + range: nodeidentifier + slot_uri: ex:nodeRef + name: + range: string + slot_uri: ex:name + +classes: + Thing: + slots: + - homepage + - node_ref + - name +""" + # Default behaviour: uri → xsd:anyURI (backward compatible) + ctx_default = json.loads(ContextGenerator(schema_yaml).serialize())["@context"] + assert ctx_default["homepage"]["@type"] == "xsd:anyURI" + + # Opt-in: uri → @id (aligned with SHACL sh:nodeKind sh:IRI) + ctx_iri = json.loads(ContextGenerator(schema_yaml, xsd_anyuri_as_iri=True).serialize())["@context"] + assert ctx_iri["homepage"]["@type"] == "@id", ( + f"Expected @type: @id for uri range with xsd_anyuri_as_iri=True, got {ctx_iri['homepage'].get('@type')}" + ) + + # nodeidentifier is unaffected by the flag (not xsd:anyURI-typed) + # Its default @type depends on URI_RANGES matching shex:nonLiteral; + # we only verify the flag doesn't change its behaviour. + assert ctx_default["node_ref"]["@type"] == ctx_iri["node_ref"]["@type"] + + # string → no @type regardless of flag + assert "@type" not in ctx_default.get("name", {}) + assert "@type" not in ctx_iri.get("name", {}) + + +def test_xsd_anyuri_as_iri_with_any_of(): + """The --xsd-anyuri-as-iri flag must also apply to ``any_of`` slots + whose type branches include ``uri`` mixed with class ranges. + + ``_literal_coercion_for_ranges`` resolves mixed any_of type branches + and must use the extended URI_RANGES when the flag is active. + """ + schema_yaml = """ +id: https://example.org/test-anyof-uri +name: test_anyof_uri + +prefixes: + ex: https://example.org/ + linkml: https://w3id.org/linkml/ + +imports: + - linkml:types + +default_prefix: ex +default_range: string + +classes: + Container: + slots: + - mixed_slot + Target: + class_uri: ex:Target + +slots: + mixed_slot: + slot_uri: ex:mixed + any_of: + - range: Target + - range: uri +""" + # Default: mixed class+uri any_of — uri resolves to xsd:anyURI literal, + # which disagrees with @id from the class branch → no coercion emitted + ctx_default = json.loads(ContextGenerator(schema_yaml).serialize())["@context"] + default_type = ctx_default.get("mixed_slot", {}).get("@type") + assert default_type != "@id", f"Without flag, mixed any_of should not resolve to @id, got {default_type}" + + # With flag: uri branch now also resolves to @id, matching the class branch + # → all branches agree → @id is emitted + ctx_iri = json.loads(ContextGenerator(schema_yaml, xsd_anyuri_as_iri=True).serialize())["@context"] + assert ctx_iri["mixed_slot"]["@type"] == "@id", ( + f"Expected @id for mixed any_of with flag, got {ctx_iri.get('mixed_slot', {}).get('@type')}" + ) + + +def test_xsd_anyuri_as_iri_owl(): + """OWL generator must produce owl:ObjectProperty for uri ranges when flag is set. + + Without the flag, ``range: uri`` produces ``owl:DatatypeProperty`` with + ``rdfs:range xsd:anyURI``. With ``xsd_anyuri_as_iri=True``, it should + produce ``owl:ObjectProperty`` (no rdfs:range restriction), aligning + with the SHACL generator's ``sh:nodeKind sh:IRI``. + """ + from rdflib import OWL, RDF, URIRef + + from linkml.generators.owlgen import OwlSchemaGenerator + + schema_yaml = """ +id: https://example.org/test-owl-uri +name: test_owl_uri +prefixes: + ex: https://example.org/ + linkml: https://w3id.org/linkml/ +imports: + - linkml:types +default_prefix: ex +default_range: string +slots: + homepage: + range: uri + slot_uri: ex:homepage + name: + range: string + slot_uri: ex:name +classes: + Thing: + slots: + - homepage + - name +""" + # Default: uri → DatatypeProperty (must disable type_objects which + # unconditionally returns ObjectProperty for all type-ranged slots) + gen_default = OwlSchemaGenerator(schema_yaml, type_objects=False) + g_default = gen_default.as_graph() + homepage_uri = URIRef("https://example.org/homepage") + default_rdf_type = set(g_default.objects(homepage_uri, RDF.type)) + assert OWL.DatatypeProperty in default_rdf_type, ( + f"Without flag, homepage should be DatatypeProperty, got {default_rdf_type}" + ) + + # With flag: uri → ObjectProperty + gen_iri = OwlSchemaGenerator(schema_yaml, xsd_anyuri_as_iri=True, type_objects=False) + g_iri = gen_iri.as_graph() + iri_rdf_type = set(g_iri.objects(homepage_uri, RDF.type)) + assert OWL.ObjectProperty in iri_rdf_type, f"With flag, homepage should be ObjectProperty, got {iri_rdf_type}" + assert OWL.DatatypeProperty not in iri_rdf_type, ( + f"With flag, homepage should NOT be DatatypeProperty, got {iri_rdf_type}" + ) + + # String slot must remain DatatypeProperty regardless of flag + name_uri = URIRef("https://example.org/name") + name_rdf_type = set(g_iri.objects(name_uri, RDF.type)) + assert OWL.DatatypeProperty in name_rdf_type, f"String slot should remain DatatypeProperty, got {name_rdf_type}" + + +def test_xsd_anyuri_as_iri_uriorcurie_range(): + """``uriorcurie`` also maps to ``xsd:anyURI`` and must behave identically + to ``uri`` when the ``--xsd-anyuri-as-iri`` flag is active. + + This is a high-priority coverage gap: ``uriorcurie`` is distinct from + ``uri`` at the LinkML level but shares the same XSD type. + """ + schema_yaml = """ +id: https://example.org/test-uriorcurie +name: test_uriorcurie + +prefixes: + ex: https://example.org/ + linkml: https://w3id.org/linkml/ + +imports: + - linkml:types + +default_prefix: ex +default_range: string + +slots: + reference: + range: uriorcurie + slot_uri: ex:reference + homepage: + range: uri + slot_uri: ex:homepage + +classes: + Thing: + slots: + - reference + - homepage +""" + ctx_default = json.loads(ContextGenerator(schema_yaml).serialize())["@context"] + assert ctx_default["reference"]["@type"] == "xsd:anyURI" + assert ctx_default["homepage"]["@type"] == "xsd:anyURI" + + ctx_iri = json.loads(ContextGenerator(schema_yaml, xsd_anyuri_as_iri=True).serialize())["@context"] + assert ctx_iri["reference"]["@type"] == "@id", "uriorcurie should map to @id with xsd_anyuri_as_iri=True" + assert ctx_iri["homepage"]["@type"] == "@id", "uri should map to @id with xsd_anyuri_as_iri=True" + + +def test_xsd_anyuri_as_iri_curie_range_unchanged(): + """``curie`` maps to ``xsd:string`` (not ``xsd:anyURI``), so the flag + must NOT affect its coercion. + + This documents the cross-type boundary: ``uri`` and ``uriorcurie`` + share ``xsd:anyURI``, but ``curie`` uses ``xsd:string``. + """ + schema_yaml = """ +id: https://example.org/test-curie +name: test_curie + +prefixes: + ex: https://example.org/ + linkml: https://w3id.org/linkml/ + +imports: + - linkml:types + +default_prefix: ex +default_range: string + +slots: + curie_slot: + range: curie + slot_uri: ex:curieSlot + uri_slot: + range: uri + slot_uri: ex:uriSlot + +classes: + Thing: + slots: + - curie_slot + - uri_slot +""" + ctx_default = json.loads(ContextGenerator(schema_yaml).serialize())["@context"] + ctx_iri = json.loads(ContextGenerator(schema_yaml, xsd_anyuri_as_iri=True).serialize())["@context"] + + # curie (xsd:string) must be unaffected by the flag + curie_default = ctx_default.get("curie_slot", {}).get("@type") + curie_iri = ctx_iri.get("curie_slot", {}).get("@type") + assert curie_default == curie_iri, f"curie coercion should not change with flag: {curie_default} vs {curie_iri}" + + # uri (xsd:anyURI) must change — sanity check + assert ctx_iri["uri_slot"]["@type"] == "@id" + + +def test_xsd_anyuri_as_iri_owl_curie_unchanged(): + """OWL generator must keep ``range: curie`` as DatatypeProperty even with flag. + + ``curie`` maps to ``xsd:string`` (not ``xsd:anyURI``), so the + ``--xsd-anyuri-as-iri`` flag must not promote it to ObjectProperty. + This verifies cross-generator consistency: the JSON-LD context generator + already correctly excludes ``curie`` via ``URI_RANGES_WITH_XSD``; the + OWL generator must match via ``is_xsd_anyuri_range()``. + """ + from rdflib import OWL, RDF, URIRef + + from linkml.generators.owlgen import OwlSchemaGenerator + + schema_yaml = """ +id: https://example.org/test-owl-curie +name: test_owl_curie +prefixes: + ex: https://example.org/ + linkml: https://w3id.org/linkml/ +imports: + - linkml:types +default_prefix: ex +default_range: string +slots: + compact_id: + range: curie + slot_uri: ex:compactId + homepage: + range: uri + slot_uri: ex:homepage +classes: + Thing: + slots: + - compact_id + - homepage +""" + compact_id_uri = URIRef("https://example.org/compact_id") + homepage_uri = URIRef("https://example.org/homepage") + + # With flag: curie must stay DatatypeProperty, uri must become ObjectProperty + gen = OwlSchemaGenerator(schema_yaml, xsd_anyuri_as_iri=True, type_objects=False) + g = gen.as_graph() + + curie_types = set(g.objects(compact_id_uri, RDF.type)) + assert OWL.DatatypeProperty in curie_types, f"curie slot must remain DatatypeProperty with flag, got {curie_types}" + assert OWL.ObjectProperty not in curie_types, ( + f"curie slot must NOT become ObjectProperty with flag, got {curie_types}" + ) + + # Sanity: uri must become ObjectProperty + uri_types = set(g.objects(homepage_uri, RDF.type)) + assert OWL.ObjectProperty in uri_types, f"uri slot should be ObjectProperty with flag, got {uri_types}" + + +def test_xsd_anyuri_as_iri_cli_flag(): + """Verify the ``--xsd-anyuri-as-iri`` flag is wired through Click.""" + import tempfile + from pathlib import Path + + from click.testing import CliRunner + + from linkml.generators.jsonldcontextgen import cli + + schema_yaml = """ +id: https://example.org/test-cli +name: test_cli + +prefixes: + ex: https://example.org/ + linkml: https://w3id.org/linkml/ + +imports: + - linkml:types + +default_prefix: ex +default_range: string + +slots: + homepage: + range: uri + slot_uri: ex:homepage + +classes: + Thing: + slots: + - homepage +""" + with tempfile.TemporaryDirectory() as tmpdir: + schema_path = Path(tmpdir) / "test.yaml" + schema_path.write_text(schema_yaml) + + runner = CliRunner() + + # Without flag + result_default = runner.invoke(cli, [str(schema_path)]) + assert result_default.exit_code == 0, result_default.output + ctx_default = json.loads(result_default.output)["@context"] + assert ctx_default["homepage"]["@type"] == "xsd:anyURI" + + # With flag + result_iri = runner.invoke(cli, [str(schema_path), "--xsd-anyuri-as-iri"]) + assert result_iri.exit_code == 0, result_iri.output + ctx_iri = json.loads(result_iri.output)["@context"] + assert ctx_iri["homepage"]["@type"] == "@id"