Repository: equinor/fmu-dataio

Issue:
Add data.property to schema
_Related to Issue #734_

**Add data.property to schema:**
We are currently telling people to use a dictionary form with extra info (attribute and is_discrete) but any extra info will be dropped since a `data.property` is not defined in the schema.

Add `data.property` as optional field for data of content `property`
Add `data.property.attribute` as optional field.
Add `data.property.is_discrete` as optional field.

Hints from issue comments:

Solution patch:
diff --git a/docs/src/dataio_3_migration.rst b/docs/src/dataio_3_migration.rst
index 540ff8d66..3f596772c 100644
--- a/docs/src/dataio_3_migration.rst
+++ b/docs/src/dataio_3_migration.rst
@@ -23,10 +23,10 @@ but with replacements inplace.
  - ``content`` was previously optional, it should now be explicitly provided as a valid content string.
  - ``content`` no longer supports using a dictionary form to provide extra information together
    with the ``content``, use the ``content_metadata`` argument instead.
+ - when ``content='property'`` it will be required to set the ``attribute`` through the ``content_metadata``.
  - ``content={'seismic': {'offset': '0-15'}}`` no longer works, use the key ``stacking_offset`` instead 
    of ``offset``.
 
-
 Following are an example demonstrating several deprecated patterns:
 
 .. code-block:: python
diff --git a/examples/example_metadata/fmu_case.yml b/examples/example_metadata/fmu_case.yml
index ef067a4aa..18598aca9 100644
--- a/examples/example_metadata/fmu_case.yml
+++ b/examples/example_metadata/fmu_case.yml
@@ -32,7 +32,7 @@ masterdata:
       uuid: ad214d85-8a1d-19da-e053-c918a4889310
 source: fmu
 tracklog:
-- datetime: '2025-03-25T09:30:43.827177Z'
+- datetime: '2025-03-27T09:24:43.814407Z'
   event: created
   sysinfo:
     fmu-dataio:
diff --git a/examples/example_metadata/polygons_field_outline.yml b/examples/example_metadata/polygons_field_outline.yml
index f6617ea89..561d6795e 100644
--- a/examples/example_metadata/polygons_field_outline.yml
+++ b/examples/example_metadata/polygons_field_outline.yml
@@ -93,7 +93,7 @@ masterdata:
       uuid: ad214d85-8a1d-19da-e053-c918a4889310
 source: fmu
 tracklog:
-- datetime: '2025-03-25T09:30:48.655241Z'
+- datetime: '2025-03-27T09:24:47.339602Z'
   event: created
   sysinfo:
     fmu-dataio:
diff --git a/examples/example_metadata/polygons_field_region.yml b/examples/example_metadata/polygons_field_region.yml
index 88a1ba5b1..437260704 100644
--- a/examples/example_metadata/polygons_field_region.yml
+++ b/examples/example_metadata/polygons_field_region.yml
@@ -93,7 +93,7 @@ masterdata:
       uuid: ad214d85-8a1d-19da-e053-c918a4889310
 source: fmu
 tracklog:
-- datetime: '2025-03-25T09:30:48.632461Z'
+- datetime: '2025-03-27T09:24:47.319069Z'
   event: created
   sysinfo:
     fmu-dataio:
diff --git a/examples/example_metadata/preprocessed_surface_depth.yml b/examples/example_metadata/preprocessed_surface_depth.yml
index bf82d2ca4..236c4bf9e 100644
--- a/examples/example_metadata/preprocessed_surface_depth.yml
+++ b/examples/example_metadata/preprocessed_surface_depth.yml
@@ -80,7 +80,7 @@ masterdata:
       uuid: ad214d85-8a1d-19da-e053-c918a4889310
 source: fmu
 tracklog:
-- datetime: '2025-03-25T09:30:55.744852Z'
+- datetime: '2025-03-27T09:24:52.514594Z'
   event: created
   sysinfo:
     fmu-dataio:
diff --git a/examples/example_metadata/surface_depth.yml b/examples/example_metadata/surface_depth.yml
index 37bf500e6..606cbc599 100644
--- a/examples/example_metadata/surface_depth.yml
+++ b/examples/example_metadata/surface_depth.yml
@@ -90,7 +90,7 @@ masterdata:
       uuid: ad214d85-8a1d-19da-e053-c918a4889310
 source: fmu
 tracklog:
-- datetime: '2025-03-25T09:30:57.993616Z'
+- datetime: '2025-03-27T09:24:54.234957Z'
   event: created
   sysinfo:
     fmu-dataio:
diff --git a/examples/example_metadata/surface_fluid_contact.yml b/examples/example_metadata/surface_fluid_contact.yml
index 262547d43..870bb4c53 100644
--- a/examples/example_metadata/surface_fluid_contact.yml
+++ b/examples/example_metadata/surface_fluid_contact.yml
@@ -93,7 +93,7 @@ masterdata:
       uuid: ad214d85-8a1d-19da-e053-c918a4889310
 source: fmu
 tracklog:
-- datetime: '2025-03-25T09:30:58.045815Z'
+- datetime: '2025-03-27T09:24:54.278768Z'
   event: created
   sysinfo:
     fmu-dataio:
diff --git a/examples/example_metadata/surface_seismic_amplitude.yml b/examples/example_metadata/surface_seismic_amplitude.yml
index c39dfa76a..a0eb64828 100644
--- a/examples/example_metadata/surface_seismic_amplitude.yml
+++ b/examples/example_metadata/surface_seismic_amplitude.yml
@@ -111,7 +111,7 @@ masterdata:
       uuid: ad214d85-8a1d-19da-e053-c918a4889310
 source: fmu
 tracklog:
-- datetime: '2025-03-25T09:30:58.100840Z'
+- datetime: '2025-03-27T09:24:54.324195Z'
   event: created
   sysinfo:
     fmu-dataio:
diff --git a/examples/example_metadata/table_inplace_volumes.yml b/examples/example_metadata/table_inplace_volumes.yml
index 8dec82b6f..494fa777b 100644
--- a/examples/example_metadata/table_inplace_volumes.yml
+++ b/examples/example_metadata/table_inplace_volumes.yml
@@ -98,7 +98,7 @@ masterdata:
       uuid: ad214d85-8a1d-19da-e053-c918a4889310
 source: fmu
 tracklog:
-- datetime: '2025-03-25T09:31:03.224361Z'
+- datetime: '2025-03-27T09:24:58.433947Z'
   event: created
   sysinfo:
     fmu-dataio:
diff --git a/schemas/0.10.0/fmu_results.json b/schemas/0.10.0/fmu_results.json
index 2f0f6eb19..1b53d756b 100644
--- a/schemas/0.10.0/fmu_results.json
+++ b/schemas/0.10.0/fmu_results.json
@@ -5604,6 +5604,40 @@
       "title": "PolygonsSpecification",
       "type": "object"
     },
+    "Property": {
+      "description": "A block describing property data. Shall be present if ``data.content`\n== ``property``.",
+      "properties": {
+        "attribute": {
+          "anyOf": [
+            {
+              "type": "string"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "examples": [
+            "porosity"
+          ],
+          "title": "Attribute"
+        },
+        "is_discrete": {
+          "anyOf": [
+            {
+              "type": "boolean"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "title": "Is Discrete"
+        }
+      },
+      "title": "Property",
+      "type": "object"
+    },
     "PropertyData": {
       "description": "The ``data`` block contains information about the data contained in this object.\nThis class contains metadata for property data.",
       "properties": {
@@ -5747,6 +5781,17 @@
           "title": "Offset",
           "type": "number"
         },
+        "property": {
+          "anyOf": [
+            {
+              "$ref": "#/$defs/Property"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null
+        },
         "spec": {
           "anyOf": [
             {
diff --git a/src/fmu/dataio/_models/fmu_results/data.py b/src/fmu/dataio/_models/fmu_results/data.py
index fd92d2442..741c10f22 100644
--- a/src/fmu/dataio/_models/fmu_results/data.py
+++ b/src/fmu/dataio/_models/fmu_results/data.py
@@ -75,6 +75,17 @@ class Seismic(BaseModel):
     """The z-range of these data."""
 
 
+class Property(BaseModel):
+    """A block describing property data. Shall be present if ``data.content`
+    == ``property``."""
+
+    attribute: str | None = Field(default=None, examples=["porosity"])
+    """A known attribute."""
+
+    is_discrete: bool | None = Field(default=None)
+    """If True, this is a discrete property."""
+
+
 class FluidContact(BaseModel):
     """
     A block describing a fluid contact. Shall be present if ``data.content``
@@ -466,6 +477,8 @@ class PropertyData(Data):
     content: Literal[enums.Content.property]
     """The type of content these data represent."""
 
+    property: Property | None = Field(default=None)
+
 
 class PVTData(Data):
     """
diff --git a/src/fmu/dataio/_models/fmu_results/fmu_results.py b/src/fmu/dataio/_models/fmu_results/fmu_results.py
index 94c6c33c2..c588bc8c3 100644
--- a/src/fmu/dataio/_models/fmu_results/fmu_results.py
+++ b/src/fmu/dataio/_models/fmu_results/fmu_results.py
@@ -49,6 +49,9 @@ class FmuResultsSchema(SchemaBase):
     VERSION_CHANGELOG: str = """
     #### 0.10.0
 
+    - `data.property` added as optional field for data of content `property`
+    - `data.property.attribute` added as optional field.
+    - `data.property.is_discrete` added as optional field.
     - `data.standard_result` now supports `StructureDepthIsochoreStandardResult`
     - `data.standard_result` now supports `StructureDepthFaultLinesStandardResult`
     - `data.spec.columns` added as optional field for points, polygons
diff --git a/src/fmu/dataio/providers/objectdata/_export_models.py b/src/fmu/dataio/providers/objectdata/_export_models.py
index dc96f2cd3..0c85503df 100644
--- a/src/fmu/dataio/providers/objectdata/_export_models.py
+++ b/src/fmu/dataio/providers/objectdata/_export_models.py
@@ -57,12 +57,6 @@ class AllowedContentSeismic(data.Seismic):
         return self
 
 
-class AllowedContentProperty(BaseModel):
-    # needs to be here for now, as it is not defined in the schema
-    attribute: str | None = Field(default=None)
-    is_discrete: bool | None = Field(default=None)
-
-
 class UnsetData(data.Data):
     content: Literal["unset"]  # type: ignore
 
@@ -88,7 +82,7 @@ def content_metadata_factory(content: enums.Content) -> type[BaseModel]:
     if content == enums.Content.fluid_contact:
         return data.FluidContact
     if content == enums.Content.property:
-        return AllowedContentProperty
+        return data.Property
     if content == enums.Content.seismic:
         return AllowedContentSeismic
     raise ValueError(f"No content_metadata model exist for content {str(content)}")


Test patch:
diff --git a/tests/test_units/test_contents.py b/tests/test_units/test_contents.py
index 9f89c185b..171b43fd3 100644
--- a/tests/test_units/test_contents.py
+++ b/tests/test_units/test_contents.py
@@ -172,7 +172,7 @@ def test_content_property(gridproperty, globalconfig2):
 def test_content_property_as_dict(gridproperty, globalconfig2):
     """Test export of the property content."""
     content_specifc = {"attribute": "porosity", "is_discrete": False}
-    # should give FutureWarning whan not linked to a grid
+    # should give FutureWarning when not linked to a grid
     with pytest.warns(FutureWarning, match="linking it to a geometry"):
         meta = ExportData(
             config=globalconfig2,
@@ -182,8 +182,7 @@ def test_content_property_as_dict(gridproperty, globalconfig2):
         ).generate_metadata(gridproperty)
 
     assert meta["data"]["content"] == "property"
-    # TODO: add next line when schema defines content_specific for property
-    # assert meta["data"]["property"] == content_specifc
+    assert meta["data"]["property"] == content_specifc
 
 
 def test_content_seismic_as_dict(gridproperty, globalconfig2):