diff --git a/dataforge-meta/build.gradle.kts b/dataforge-meta/build.gradle.kts
index 49d8bdc5..a4c67c9c 100644
--- a/dataforge-meta/build.gradle.kts
+++ b/dataforge-meta/build.gradle.kts
@@ -10,6 +10,12 @@ kscience {
useSerialization {
json()
}
+
+ commonTest {
+ dependencies {
+ implementation("io.github.optimumcode:json-schema-validator:0.5.2")
+ }
+ }
}
description = "Meta definition and basic operations on meta"
diff --git a/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/JsonSchema.kt b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/JsonSchema.kt
new file mode 100644
index 00000000..39d9e10f
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/JsonSchema.kt
@@ -0,0 +1,400 @@
+package space.kscience.dataforge.meta.descriptors
+
+/**
+ * A comprehensive JSON Schema implementation supporting Draft 2020-12 specification.
+ *
+ * This object provides:
+ * - Core JSON Schema vocabulary constants (like `$schema`, `$id`, `$ref`)
+ * - Standard validation keywords (type, enum, pattern, etc.)
+ * - Schema composition keywords (allOf, anyOf, oneOf)
+ * - Metadata annotations (title, description, examples)
+ * - Custom extensions for specialized use cases
+ * - Version management for JSON Schema specifications
+ *
+ * The structure follows the official JSON Schema organization with nested vocabulary objects:
+ * - [Vocabularies.Core]: Fundamental schema identification and referencing
+ * - [Vocabularies.Applicator]: Schema composition and combination
+ * - [Vocabularies.Validation]: Instance validation constraints
+ * - [Vocabularies.MetaData]: Descriptive metadata
+ * - [Vocabularies.Custom]: Domain-specific extensions
+ *
+ * @property VERSION The supported JSON Schema version (DRAFT_2020_12 by default)
+ *
+ * @see JSON Schema Core Specification
+ */
+public object JsonSchema {
+ public val VERSION: JsonSchemaVersion = JsonSchemaVersion.DRAFT_2020_12
+
+ /**
+ * A curated set of vocabularies defined for this JSON Schema dialect
+ */
+ public object Vocabularies {
+ /**
+ * A mandatory vocabulary that defines keywords that are either required in order to process any schema
+ * or meta-schema, including those split across multiple documents, or exist to reserve keywords
+ * for purposes that require guaranteed interoperability
+ */
+ public object Core {
+ /**
+ * This keyword declares an identifier for the schema resource
+ */
+ public const val ID: String = "\$id"
+
+ /**
+ * This keyword is both used as a JSON Schema dialect identifier and as a reference to a JSON Schema
+ * which describes the set of valid schemas written for this particular dialect
+ */
+ public const val SCHEMA: String = "\$schema"
+
+ /**
+ * This keyword is used to reference a statically identified schema
+ */
+ public const val REF: String = "\$ref"
+
+ /**
+ * This keyword reserves a location for comments from schema authors to readers or maintainers of the schema
+ */
+ public const val COMMENT: String = "\$comment"
+
+ /**
+ * This keyword reserves a location for schema authors to inline re-usable JSON Schemas into a more general schema
+ */
+ public const val DEFS: String = "\$defs"
+
+ /**
+ * This keyword is used to create plain name fragments that are not tied to any particular structural location
+ * for referencing purposes, which are taken into consideration for static referencing
+ */
+ public const val ANCHOR: String = "\$anchor"
+
+ /**
+ * This keyword is used to create plain name fragments that are not tied to any particular structural location
+ * for referencing purposes, which are taken into consideration for dynamic referencing
+ */
+ public const val DYNAMIC_ANCHOR: String = "\$dynamicAnchor"
+
+ /**
+ * This keyword is used to reference an identified schema, deferring the full resolution until runtime,
+ * at which point it is resolved each time it is encountered while evaluating an instance
+ */
+ public const val DYNAMIC_REF: String = "\$dynamicRef"
+
+ /**
+ * This keyword is used in dialect meta-schemas to identify the required and optional vocabularies
+ * available for use in schemas described by that dialect
+ */
+ public const val VOCABULARY: String = "\$vocabulary"
+ }
+
+ /**
+ * A vocabulary that defines applicator keywords that are recommended for use as the basis of other vocabularies
+ */
+ public object Applicator {
+ /**
+ * An instance validates successfully against this keyword if it validates successfully against all schemas
+ * defined by this keyword's value
+ */
+ public const val ALL_OF: String = "allOf"
+
+ /**
+ * An instance validates successfully against this keyword if it validates successfully against at least one
+ * schema defined by this keyword's value
+ */
+ public const val ANY_OF: String = "anyOf"
+
+ /**
+ * An instance validates successfully against this keyword if it validates successfully against exactly one
+ * schema defined by this keyword's value
+ */
+ public const val ONE_OF: String = "oneOf"
+
+ /**
+ * This keyword declares a condition based on the validation result of the given schema
+ */
+ public const val IF: String = "if"
+
+ /**
+ * When if is present, and the instance successfully validates against its subschema, then validation succeeds
+ * if the instance also successfully validates against this keyword's subschema
+ */
+ public const val THEN: String = "then"
+
+ /**
+ * When if is present, and the instance fails to validate against its subschema, then validation succeeds
+ * if the instance successfully validates against this keyword's subschema
+ */
+ public const val ELSE: String = "else"
+
+ /**
+ * An instance is valid against this keyword if it fails to validate successfully against the schema
+ * defined by this keyword
+ */
+ public const val NOT: String = "not"
+
+ /**
+ * Validation succeeds if, for each name that appears in both the instance and as a name within this keyword's
+ * value, the child instance for that name successfully validates against the corresponding schema
+ */
+ public const val PROPERTIES: String = "properties"
+
+ /**
+ * Validation succeeds if the schema validates against each value not matched by other object applicators
+ * in this vocabulary
+ */
+ public const val ADDITIONAL_PROPERTIES: String = "additionalProperties"
+
+ /**
+ * Validation succeeds if, for each instance name that matches any regular expressions that appear as a
+ * property name in this keyword's value, the child instance for that name successfully validates against
+ * each schema that corresponds to a matching regular expression
+ */
+ public const val PATTERN_PROPERTIES: String = "patternProperties"
+
+ /**
+ * This keyword specifies subschemas that are evaluated if the instance is an object and contains a certain property
+ */
+ public const val DEPENDENT_SCHEMAS: String = "dependentSchemas"
+
+ /**
+ * Validation succeeds if the schema validates against every property name in the instance
+ */
+ public const val PROPERTY_NAMES: String = "propertyNames"
+
+ /**
+ * Validation succeeds if the instance contains an element that validates against this schema
+ */
+ public const val CONTAINS: String = "contains"
+
+ /**
+ * Validation succeeds if each element of the instance not covered by prefixItems validates against this schema
+ */
+ public const val ITEMS: String = "items"
+
+ /**
+ * Validation succeeds if each element of the instance validates against the schema at the same position, if any
+ */
+ public const val PREFIX_ITEMS: String = "prefixItems"
+ }
+
+ /**
+ * A vocabulary that defines keywords that impose requirements for successful validation of an instance
+ */
+ public object Validation {
+ /**
+ * Validation succeeds if the type of the instance matches the type represented by the given type,
+ * or matches at least one of the given types
+ */
+ public const val TYPE: String = "type"
+
+ /**
+ * Validation succeeds if the instance is equal to one of the elements in this keyword's array value
+ */
+ public const val ENUM: String = "enum"
+
+ /**
+ * Validation succeeds if the instance is equal to this keyword's value
+ */
+ public const val CONST: String = "const"
+
+ /**
+ * A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword
+ */
+ public const val MAX_LENGTH: String = "maxLength"
+
+ /**
+ * A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword
+ */
+ public const val MIN_LENGTH: String = "minLength"
+
+ /**
+ * A string instance is considered valid if the regular expression matches the instance successfully
+ */
+ public const val PATTERN: String = "pattern"
+
+ /**
+ * Validation succeeds if the numeric instance is less than the given number
+ */
+ public const val EXCLUSIVE_MAXIMUM: String = "exclusiveMaximum"
+
+ /**
+ * Validation succeeds if the numeric instance is greater than the given number
+ */
+ public const val EXCLUSIVE_MINIMUM: String = "exclusiveMinimum"
+
+ /**
+ * Validation succeeds if the numeric instance is less than or equal to the given number
+ */
+ public const val MAXIMUM: String = "maximum"
+
+ /**
+ * Validation succeeds if the numeric instance is greater than or equal to the given number
+ */
+ public const val MINIMUM: String = "minimum"
+
+ /**
+ * A numeric instance is valid only if division by this keyword's value results in an integer
+ */
+ public const val MULTIPLE_OF: String = "multipleOf"
+
+ /**
+ * Validation succeeds if, for each name that appears in both the instance and as a name within this keyword's
+ * value, every item in the corresponding array is also the name of a property in the instance
+ */
+ public const val DEPENDENT_REQUIRED: String = "dependentRequired"
+
+ /**
+ * An object instance is valid if its number of properties is less than, or equal to, the value of this keyword
+ */
+ public const val MAX_PROPERTIES: String = "maxProperties"
+
+ /**
+ * An object instance is valid if its number of properties is greater than, or equal to, the value of this keyword
+ */
+ public const val MIN_PROPERTIES: String = "minProperties"
+
+ /**
+ * An object instance is valid against this keyword if every item in the array is the name of a property in the instance
+ */
+ public const val REQUIRED: String = "required"
+
+ /**
+ * An array instance is valid if its size is less than, or equal to, the value of this keyword
+ */
+ public const val MAX_ITEMS: String = "maxItems"
+
+ /**
+ * An array instance is valid if its size is greater than, or equal to, the value of this keyword
+ */
+ public const val MIN_ITEMS: String = "minItems"
+
+ /**
+ * The number of times that the contains keyword (if set) successfully validates against the instance must be
+ * less than or equal to the given integer
+ */
+ public const val MAX_CONTAINS: String = "maxContains"
+
+ /**
+ * The number of times that the contains keyword (if set) successfully validates against the instance must be
+ * greater than or equal to the given integer
+ */
+ public const val MIN_CONTAINS: String = "minContains"
+
+ /**
+ * If this keyword is set to the boolean value true, the instance validates successfully if all of its elements are unique
+ */
+ public const val UNIQUE_ITEMS: String = "uniqueItems"
+ }
+
+ /**
+ * A vocabulary to defines general-purpose annotation keywords
+ */
+ public object MetaData {
+ /**
+ * A preferably short description about the purpose of the instance described by the schema
+ */
+ public const val TITLE: String = "title"
+
+ /**
+ * An explanation about the purpose of the instance described by the schema
+ */
+ public const val DESCRIPTION: String = "description"
+
+ /**
+ * This keyword can be used to supply a default JSON value associated with a particular schema
+ */
+ public const val DEFAULT: String = "default"
+
+ /**
+ * This keyword indicates that applications should refrain from using the declared property
+ */
+ public const val DEPRECATED: String = "deprecated"
+
+ /**
+ * This keyword is used to provide sample JSON values associated with a particular schema
+ */
+ public const val EXAMPLES: String = "examples"
+
+ /**
+ * This keyword indicates that the value of the instance is managed exclusively by the owning authority
+ */
+ public const val READ_ONLY: String = "readOnly"
+
+ /**
+ * This keyword indicates that the value is never present when the instance is retrieved
+ */
+ public const val WRITE_ONLY: String = "writeOnly"
+ }
+
+ /**
+ * A vocabulary to defines semantic information about string-encoded values
+ */
+ public object FormatAnnotation {
+ /**
+ * Define semantic information about a string instance
+ */
+ public const val FORMAT: String = "format"
+ }
+
+ /**
+ * A vocabulary for annotating instances that contain non-JSON data encoded in JSON strings
+ */
+ public object Content {
+ /**
+ * The string instance should be interpreted as encoded binary data
+ */
+ public const val CONTENT_ENCODING: String = "contentEncoding"
+
+ /**
+ * This keyword declares the media type of the string instance
+ */
+ public const val CONTENT_MEDIA_TYPE: String = "contentMediaType"
+
+ /**
+ * This keyword declares a schema which describes the structure of the string
+ */
+ public const val CONTENT_SCHEMA: String = "contentSchema"
+ }
+
+ /**
+ * A vocabulary for applying subschemas to unevaluated array items or object properties
+ */
+ public object Unevaluated {
+ /**
+ * Validates array elements that did not successfully validate against other standard array applicators
+ */
+ public const val UNEVALUATED_ITEMS: String = "unevaluatedItems"
+
+ /**
+ * Validates object properties that did not successfully validate against other standard object applicators
+ */
+ public const val UNEVALUATED_PROPERTIES: String = "unevaluatedProperties"
+ }
+
+ public object Custom {
+ /**
+ * An index field by which this node is identified in case of same name siblings construct
+ */
+ public const val INDEX_KEY: String = "__indexKey__"
+
+ /**
+ * Additional attributes of this descriptor. For example, validation and widget parameters
+ */
+ public const val ATTRIBUTES: String = "__attributes__"
+
+ /**
+ * True if same name siblings with this name are allowed
+ */
+ public const val MULTIPLE: String = "__multiple__"
+ }
+ }
+
+ public enum class JsonSchemaVersion(public val value: String) {
+ /**
+ * JSON Schema 2020-12 is a JSON media type for defining the structure of JSON data.
+ * JSON Schema is intended to define validation, documentation, hyperlink navigation,
+ * and interaction control of JSON data.
+ * @see Specification
+ */
+ DRAFT_2020_12("https://json-schema.org/draft/2020-12/schema")
+ }
+}
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptor.kt b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptor.kt
index 45954985..7930b579 100644
--- a/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptor.kt
+++ b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptor.kt
@@ -71,7 +71,14 @@ public data class MetaDescriptor(
}
}
-public val MetaDescriptor.required: Boolean get() = valueRestriction == ValueRestriction.REQUIRED || nodes.values.any { required }
+public val MetaDescriptor.required: Boolean get() = checkRequired(hashSetOf())
+
+private fun MetaDescriptor.checkRequired(visited: MutableSet): Boolean {
+ if (this in visited) return false
+ visited.add(this)
+ return valueRestriction == ValueRestriction.REQUIRED ||
+ nodes.values.any { it.checkRequired(visited) }
+}
public val MetaDescriptor.allowedValues: List? get() = attributes[MetaDescriptor.ALLOWED_VALUES_KEY]?.value?.list
diff --git a/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptorJsonSchema.kt b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptorJsonSchema.kt
new file mode 100644
index 00000000..224a903c
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptorJsonSchema.kt
@@ -0,0 +1,223 @@
+package space.kscience.dataforge.meta.descriptors
+
+import kotlinx.serialization.json.*
+import space.kscience.dataforge.meta.*
+
+/**
+ * A converter between [MetaDescriptor] and JSON Schema ([JsonObject]) representations.
+ *
+ * Provides bidirectional conversion between metadata descriptors and JSON Schema format,
+ * handling:
+ * - Basic schema metadata (title, description)
+ * - Value type restrictions and validation
+ * - Allowed values (enums)
+ * - Required fields
+ * - Nested properties
+ * - Default values
+ * - Custom metadata fields (indexKey, multiple, attributes)
+ *
+ * The converter maintains JSON Schema compatibility while preserving all [MetaDescriptor] features.
+ */
+private object MetaDescriptorJsonSchemaConverter {
+ fun convertMetaDescriptorToJsonSchema(metaDescriptor: MetaDescriptor): JsonObject {
+ return convertMetaDescriptorToJsonSchema(metaDescriptor, 1, null)
+ }
+
+ private fun convertMetaDescriptorToJsonSchema(metaDescriptor: MetaDescriptor, depth: Int, title: String?): JsonObject = buildJsonObject {
+ // Basic metadata
+ if (depth == 1) {
+ put(JsonSchema.Vocabularies.Core.SCHEMA, JsonSchema.VERSION.value)
+ }
+
+ title?.let { put(JsonSchema.Vocabularies.MetaData.TITLE, it) }
+ metaDescriptor.description?.let { put(JsonSchema.Vocabularies.MetaData.DESCRIPTION, it) }
+
+ // Value type handling
+ when (metaDescriptor.valueRestriction) {
+ ValueRestriction.ABSENT -> put(JsonSchema.Vocabularies.Validation.TYPE, JsonPrimitive("null"))
+ else -> {
+ metaDescriptor.valueTypes?.let { types ->
+ put(
+ JsonSchema.Vocabularies.Validation.TYPE,
+ if (types.size == 1) JsonPrimitive(valueTypeToJsonType(types[0]))
+ else buildJsonArray { types.mapToJsonTypes().forEach(::add) }
+ )
+ }
+ }
+ }
+
+ // Handle allowed values
+ metaDescriptor.allowedValues?.let { allowed ->
+ put(JsonSchema.Vocabularies.Validation.ENUM, buildJsonArray {
+ allowed.map { it.mapToJsonElement() }.forEach(::add)
+ })
+ }
+
+ // Handle required fields
+ val listOfRequired = metaDescriptor.nodes.filter { (_, node) -> node.required }.map { (name, _) -> name }
+ if (!listOfRequired.isEmpty()) {
+ put(JsonSchema.Vocabularies.Validation.REQUIRED, buildJsonArray { listOfRequired.forEach(::add) })
+ }
+
+ // Handle child nodes
+ if (metaDescriptor.nodes.isNotEmpty()) {
+ put(
+ JsonSchema.Vocabularies.Applicator.PROPERTIES, buildJsonObject {
+ metaDescriptor.nodes.forEach { (title, node) ->
+ put(title, convertMetaDescriptorToJsonSchema(node, depth + 1, title))
+ }
+ })
+ }
+
+ // Handle default value
+ if (metaDescriptor.defaultValue != null) {
+ put(JsonSchema.Vocabularies.MetaData.DEFAULT, metaDescriptor.defaultValue.mapToJsonElement())
+ }
+
+ // Custom meta descriptor fields
+ put(JsonSchema.Vocabularies.Custom.INDEX_KEY, metaDescriptor.indexKey)
+ put(JsonSchema.Vocabularies.Custom.MULTIPLE, metaDescriptor.multiple)
+ put(JsonSchema.Vocabularies.Custom.ATTRIBUTES, Json.encodeToJsonElement(metaDescriptor.attributes))
+ }
+
+ fun convertJsonSchemaToMetaDescriptor(jsonObject: JsonObject): MetaDescriptor {
+ return convertJsonSchemaToMetaDescriptor(jsonObject, 1)
+ }
+
+ private fun convertJsonSchemaToMetaDescriptor(jsonObject: JsonObject, depth: Int): MetaDescriptor {
+ val builder = MetaDescriptorBuilder()
+
+ // Handle basic metadata
+ builder.description = jsonObject[JsonSchema.Vocabularies.MetaData.DESCRIPTION]?.jsonPrimitive?.contentOrNull
+
+ // Handle value types
+ val typeElement = jsonObject[JsonSchema.Vocabularies.Validation.TYPE]
+ val valueTypes = when (typeElement) {
+ is JsonPrimitive -> listOfNotNull(jsonTypeToValueType(typeElement.contentOrNull))
+ is JsonArray -> typeElement.mapNotNull { jsonTypeToValueType(it.jsonPrimitive.contentOrNull) }
+ else -> null
+ }
+ builder.valueTypes = valueTypes?.takeIf { it.isNotEmpty() }
+
+ // Handle value restriction
+ builder.valueRestriction = when {
+ valueTypes?.contains(ValueType.NULL) == true && valueTypes.size == 1 -> ValueRestriction.ABSENT
+ jsonObject[JsonSchema.Vocabularies.Validation.REQUIRED] != null -> ValueRestriction.REQUIRED
+ else -> ValueRestriction.NONE
+ }
+
+ // Handle allowed values
+ jsonObject[JsonSchema.Vocabularies.Validation.ENUM]?.jsonArray?.let { enumArray ->
+ builder.allowedValues = enumArray.map { jsonElement ->
+ when (jsonElement) {
+ is JsonPrimitive -> when {
+ jsonElement.isString -> StringValue(jsonElement.content)
+ jsonElement.booleanOrNull != null -> if (jsonElement.boolean) True else False
+ jsonElement.doubleOrNull != null -> NumberValue(jsonElement.double)
+ else -> Null
+ }
+ is JsonArray -> ListValue(jsonElement.map { it.mapToValue() })
+ else -> Null
+ }
+ }
+ }
+
+ // Handle default value
+ jsonObject[JsonSchema.Vocabularies.MetaData.DEFAULT]?.let { defaultValue ->
+ builder.default = defaultValue.mapToValue()
+ }
+
+ // Handle child nodes
+ jsonObject[JsonSchema.Vocabularies.Applicator.PROPERTIES]?.jsonObject?.let { properties ->
+ properties.forEach { (name, schema) ->
+ builder.node(name, convertJsonSchemaToMetaDescriptor(schema.jsonObject, depth + 1))
+ }
+ }
+
+ // Handle required fields
+ val requiredFields = jsonObject[JsonSchema.Vocabularies.Validation.REQUIRED]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList()
+ if (requiredFields.isNotEmpty()) {
+ builder.children.forEach { (name, childBuilder) ->
+ if (name in requiredFields) {
+ childBuilder.valueRestriction = ValueRestriction.REQUIRED
+ }
+ }
+ }
+
+ // Handle custom fields
+ builder.indexKey = jsonObject[JsonSchema.Vocabularies.Custom.INDEX_KEY]?.jsonPrimitive?.contentOrNull ?: Meta.INDEX_KEY
+ builder.multiple = jsonObject[JsonSchema.Vocabularies.Custom.MULTIPLE]?.jsonPrimitive?.booleanOrNull ?: false
+ jsonObject[JsonSchema.Vocabularies.Custom.ATTRIBUTES]?.jsonObject?.let { attributes ->
+ builder.attributes.update(Json.decodeFromJsonElement(attributes))
+ }
+
+ return builder.build()
+ }
+
+ /**
+ * Convert [ValueType] to JSON Schema type string
+ */
+ private fun valueTypeToJsonType(valueType: ValueType): String = when (valueType) {
+ ValueType.NUMBER -> "number"
+ ValueType.STRING -> "string"
+ ValueType.BOOLEAN -> "boolean"
+ ValueType.LIST -> "array"
+ ValueType.NULL -> "null"
+ }
+
+ /**
+ * Convert JSON Schema type string to [ValueType]
+ */
+ private fun jsonTypeToValueType(type: String?): ValueType? = when (type) {
+ "number" -> ValueType.NUMBER
+ "string" -> ValueType.STRING
+ "boolean" -> ValueType.BOOLEAN
+ "array" -> ValueType.LIST
+ "null" -> ValueType.NULL
+ else -> null
+ }
+
+ private fun List.mapToJsonTypes() = map { valueTypeToJsonType(it) }
+
+ private fun Value.mapToJsonElement(): JsonElement = when (type) {
+ ValueType.NUMBER -> JsonPrimitive(number)
+ ValueType.STRING -> JsonPrimitive(string)
+ ValueType.BOOLEAN -> JsonPrimitive(boolean)
+ ValueType.LIST -> buildJsonArray { list.map { it.mapToJsonElement() }.forEach(::add) }
+ ValueType.NULL -> JsonNull
+ }
+
+ private fun JsonElement.mapToValue(): Value = when (this) {
+ is JsonPrimitive -> when {
+ isString -> StringValue(content)
+ booleanOrNull != null -> if (boolean) True else False
+ doubleOrNull != null -> NumberValue(double)
+ intOrNull != null -> NumberValue(int)
+ longOrNull != null -> NumberValue(long)
+ else -> Null
+ }
+ is JsonArray -> ListValue(map { it.mapToValue() })
+ is JsonObject -> {
+ val meta = MutableMeta()
+ forEach { (key, value) ->
+ meta[key] = value.mapToValue()
+ }
+ meta
+ }
+ JsonNull -> Null
+ } as Value
+}
+
+/**
+ * Convert [MetaDescriptor] to a JSON Schema [JsonObject]
+ */
+public fun MetaDescriptor.toJsonSchema(): JsonObject {
+ return MetaDescriptorJsonSchemaConverter.convertMetaDescriptorToJsonSchema(this)
+}
+
+/**
+ * Convert JSON Schema [JsonObject] to [MetaDescriptor]
+ */
+public fun JsonObject.toMetaDescriptor(): MetaDescriptor {
+ return MetaDescriptorJsonSchemaConverter.convertJsonSchemaToMetaDescriptor(this)
+}
diff --git a/dataforge-meta/src/commonTest/kotlin/space/kscience/dataforge/meta/descriptors/DescriptorJsonSchemaTest.kt b/dataforge-meta/src/commonTest/kotlin/space/kscience/dataforge/meta/descriptors/DescriptorJsonSchemaTest.kt
new file mode 100644
index 00000000..2b89840d
--- /dev/null
+++ b/dataforge-meta/src/commonTest/kotlin/space/kscience/dataforge/meta/descriptors/DescriptorJsonSchemaTest.kt
@@ -0,0 +1,66 @@
+package space.kscience.dataforge.meta.descriptors
+
+import io.github.optimumcode.json.schema.JsonSchema
+import space.kscience.dataforge.meta.ValueType
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class DescriptorJsonSchemaTest {
+
+ val descriptor = MetaDescriptor {
+ node("aNode") {
+ description = "A root demo node"
+ value("b", ValueType.NUMBER) {
+ description = "b number value"
+ required()
+ }
+ node("otherNode") {
+ value("otherValue", ValueType.BOOLEAN) {
+ default(false)
+ description = "default value"
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testIsJsonSchemaValid() {
+ // Arrange
+ val descriptorJsonSchema = descriptor.toJsonSchema()
+
+ // Act & Assert
+ JsonSchema.fromJsonElement(descriptorJsonSchema)
+ }
+
+ @Test
+ fun testIsJsonSchemaConvertsToMetaDescriptor() {
+ // Arrange
+ val descriptorJsonSchema = descriptor.toJsonSchema()
+
+ // Act & Assert
+ descriptorJsonSchema.toMetaDescriptor()
+ }
+
+ @Test
+ fun testIsSerializationAndDeserializationWorksCorrect() {
+ // Arrange
+ val descriptorWithExplicitRequired = descriptor.applyRequiredRestrictions()
+ val descriptorJsonSchema = descriptorWithExplicitRequired.toJsonSchema()
+
+ // Act
+ val descriptorFromJsonSchema = descriptorJsonSchema.toMetaDescriptor()
+
+ // Assert
+ assertEquals(descriptorWithExplicitRequired, descriptorFromJsonSchema, "Expected equal descriptors")
+ }
+
+ fun MetaDescriptor.applyRequiredRestrictions(): MetaDescriptor =
+ this.copy {
+ if (this@applyRequiredRestrictions.required) {
+ valueRestriction = ValueRestriction.REQUIRED
+ }
+ nodes.forEach { (name, childDescriptor) ->
+ node(name, childDescriptor.applyRequiredRestrictions())
+ }
+ }
+}
\ No newline at end of file