-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove Optional
for list
items.
#286
Comments
when we export from pydantic model instances to json or dict there are two keywords who semantics vary a bit ( this proposal is fine. just wanted to make a note. related but same approach won't work: we also have an empty string issue on the ui side which may also be similar. i.e. an |
@satra Are you talking about validation at the Pydantic level or at the JSON Schema level? If you are talking about validation at the Pydantic level, we can solve this using a validator in |
@yarikoptic Here is a demo of the translation of a model in LinkML with a multivalued slot. personinfo.yamlid: https://w3id.org/linkml/examples/personinfo
name: personinfo
prefixes:
linkml: https://w3id.org/linkml/
schema: http://schema.org/
personinfo: https://w3id.org/linkml/examples/personinfo/
ORCID: https://orcid.org/
imports:
- linkml:types
default_range: string
classes:
Person:
class_uri: schema:Person
slots: ## specified as a list
- id
- full_name
- aliases
- age
slots:
id:
identifier: true
full_name:
required: true
description:
name of the person
slot_uri: schema:name
aliases: # `aliases` translated to have the type of `Optional[List[str]]` in Pydantic
multivalued: true
description:
other names for the person
age:
range: integer
description:
age of the person Run personinfo_pydantic.pyfrom __future__ import annotations
from datetime import (
datetime,
date
)
from decimal import Decimal
from enum import Enum
import re
import sys
from typing import (
Any,
ClassVar,
List,
Literal,
Dict,
Optional,
Union
)
from pydantic import (
BaseModel,
ConfigDict,
Field,
RootModel,
field_validator
)
metamodel_version = "None"
version = "None"
class ConfiguredBaseModel(BaseModel):
model_config = ConfigDict(
validate_assignment = True,
validate_default = True,
extra = "forbid",
arbitrary_types_allowed = True,
use_enum_values = True,
strict = False,
)
pass
class LinkMLMeta(RootModel):
root: Dict[str, Any] = {}
model_config = ConfigDict(frozen=True)
def __getattr__(self, key:str):
return getattr(self.root, key)
def __getitem__(self, key:str):
return self.root[key]
def __setitem__(self, key:str, value):
self.root[key] = value
def __contains__(self, key:str) -> bool:
return key in self.root
linkml_meta = LinkMLMeta({'default_prefix': 'https://w3id.org/linkml/examples/personinfo/',
'default_range': 'string',
'id': 'https://w3id.org/linkml/examples/personinfo',
'imports': ['linkml:types'],
'name': 'personinfo',
'prefixes': {'ORCID': {'prefix_prefix': 'ORCID',
'prefix_reference': 'https://orcid.org/'},
'linkml': {'prefix_prefix': 'linkml',
'prefix_reference': 'https://w3id.org/linkml/'},
'personinfo': {'prefix_prefix': 'personinfo',
'prefix_reference': 'https://w3id.org/linkml/examples/personinfo/'},
'schema': {'prefix_prefix': 'schema',
'prefix_reference': 'http://schema.org/'}},
'source_file': 'personinfo.yaml'} )
class Person(ConfiguredBaseModel):
linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta({'class_uri': 'schema:Person',
'from_schema': 'https://w3id.org/linkml/examples/personinfo'})
id: str = Field(..., json_schema_extra = { "linkml_meta": {'alias': 'id', 'domain_of': ['Person']} })
full_name: str = Field(..., description="""name of the person""", json_schema_extra = { "linkml_meta": {'alias': 'full_name', 'domain_of': ['Person'], 'slot_uri': 'schema:name'} })
aliases: Optional[List[str]] = Field(None, description="""other names for the person""", json_schema_extra = { "linkml_meta": {'alias': 'aliases', 'domain_of': ['Person']} })
age: Optional[int] = Field(None, description="""age of the person""", json_schema_extra = { "linkml_meta": {'alias': 'age', 'domain_of': ['Person']} })
# Model rebuild
# see https://pydantic-docs.helpmanual.io/usage/models/#rebuilding-a-model
Person.model_rebuild() Run personinfo.py# Auto generated from personinfo.yaml by pythongen.py version: 0.0.1
# Generation date: 2025-02-18T18:23:45
# Schema: personinfo
#
# id: https://w3id.org/linkml/examples/personinfo
# description:
# license: https://creativecommons.org/publicdomain/zero/1.0/
import dataclasses
import re
from jsonasobj2 import JsonObj, as_dict
from typing import Optional, List, Union, Dict, ClassVar, Any
from dataclasses import dataclass
from datetime import date, datetime
from linkml_runtime.linkml_model.meta import EnumDefinition, PermissibleValue, PvFormulaOptions
from linkml_runtime.utils.slot import Slot
from linkml_runtime.utils.metamodelcore import empty_list, empty_dict, bnode
from linkml_runtime.utils.yamlutils import YAMLRoot, extended_str, extended_float, extended_int
from linkml_runtime.utils.dataclass_extensions_376 import dataclasses_init_fn_with_kwargs
from linkml_runtime.utils.formatutils import camelcase, underscore, sfx
from linkml_runtime.utils.enumerations import EnumDefinitionImpl
from rdflib import Namespace, URIRef
from linkml_runtime.utils.curienamespace import CurieNamespace
from linkml_runtime.linkml_model.types import Integer, String
metamodel_version = "1.7.0"
version = None
# Overwrite dataclasses _init_fn to add **kwargs in __init__
dataclasses._init_fn = dataclasses_init_fn_with_kwargs
# Namespaces
ORCID = CurieNamespace('ORCID', 'https://orcid.org/')
LINKML = CurieNamespace('linkml', 'https://w3id.org/linkml/')
PERSONINFO = CurieNamespace('personinfo', 'https://w3id.org/linkml/examples/personinfo/')
SCHEMA = CurieNamespace('schema', 'http://schema.org/')
DEFAULT_ = PERSONINFO
# Types
# Class references
class PersonId(extended_str):
pass
@dataclass(repr=False)
class Person(YAMLRoot):
_inherited_slots: ClassVar[List[str]] = []
class_class_uri: ClassVar[URIRef] = SCHEMA["Person"]
class_class_curie: ClassVar[str] = "schema:Person"
class_name: ClassVar[str] = "Person"
class_model_uri: ClassVar[URIRef] = PERSONINFO.Person
id: Union[str, PersonId] = None
full_name: str = None
aliases: Optional[Union[str, List[str]]] = empty_list()
age: Optional[int] = None
def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
if self._is_empty(self.id):
self.MissingRequiredField("id")
if not isinstance(self.id, PersonId):
self.id = PersonId(self.id)
if self._is_empty(self.full_name):
self.MissingRequiredField("full_name")
if not isinstance(self.full_name, str):
self.full_name = str(self.full_name)
if not isinstance(self.aliases, list):
self.aliases = [self.aliases] if self.aliases is not None else []
self.aliases = [v if isinstance(v, str) else str(v) for v in self.aliases]
if self.age is not None and not isinstance(self.age, int):
self.age = int(self.age)
super().__post_init__(**kwargs)
# Enumerations
# Slots
class slots:
pass
slots.id = Slot(uri=PERSONINFO.id, name="id", curie=PERSONINFO.curie('id'),
model_uri=PERSONINFO.id, domain=None, range=URIRef)
slots.full_name = Slot(uri=SCHEMA.name, name="full_name", curie=SCHEMA.curie('name'),
model_uri=PERSONINFO.full_name, domain=None, range=str)
slots.aliases = Slot(uri=PERSONINFO.aliases, name="aliases", curie=PERSONINFO.curie('aliases'),
model_uri=PERSONINFO.aliases, domain=None, range=Optional[Union[str, List[str]]])
slots.age = Slot(uri=PERSONINFO.age, name="age", curie=PERSONINFO.curie('age'),
model_uri=PERSONINFO.age, domain=None, range=Optional[int]) As illustrated in the two translation, a slot that is multivalued and not required is translated to have a type of |
at json schema inside vjsf |
marking |
I tested this on the newer VJSF (VJSF 3). Here are my findings - The custom schema generation function that @candleindark added seems to break the schema on VJSF 3, due to "affiliation": {
"type": "string",
"default": null,
"description": "An organization that this person is affiliated with.",
"nskey": "schema",
"title": "Affiliation"
} Removing that function and regenerating the schema yields something that looks like this "affiliation": {
"anyOf": [
{
"items": {
"$ref": "#/$defs/Affiliation"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "An organization that this person is affiliated with.",
"nskey": "schema",
"title": "Affiliation"
} And while this doesn't cause Finally, I tested transforming all these "affiliation": {
"items": {
"$ref": "#/$defs/Affiliation"
},
"type": "array"
"default": [],
"description": "An organization that this person is affiliated with.",
"nskey": "schema",
"title": "Affiliation"
} And this works fine in VJSF 3. So, from my perspective concerning meditor/web app development, it seems removing |
Stems from the UI observed issue/discussion between @candleindark @waxlamp and me
where we observe an ambiguity that schema allows for two semantically identical values of
null
(nothing) and[]
(empty list) to be used.We have about 30 unique attributes of such kind used in 34 definitions (overloads or whatever)
and all of them defaulting to `None`
Why do not we drop
Optional
and just default to empty list there?@satra wdyt?
Optional[List[]]
when multivalue slot is used.The text was updated successfully, but these errors were encountered: