Repository: frictionlessdata/frictionless-py

Issue:
Provide validation hints for common mistakes?
# Overview

For example, one can define `missingValues` on the resource level instead of the schema level. It's a common mistake and according to the standards a resource having `missingValues` is perfectly valid (because props set are customizable and open-ended). In this case (and similar), the framework might emit a warning in the validation report.

---

Missed schema level (`resource -> fields` instead of `resource -> schema -> fields`) - https://github.com/frictionlessdata/frictionless-py/issues/1043

Discussion:
shashigharti: @roll  Thank you a lot! I have made the changes.

Patch:
diff --git a/data/package-with-fields-993.json b/data/package-with-fields-993.json
new file mode 100644
index 00000000..89e98b95
--- /dev/null
+++ b/data/package-with-fields-993.json
@@ -0,0 +1,13 @@
+{
+   "name":"test-tabulator",
+   "fields":[
+      {
+         "name":"id",
+         "type":"integer"
+      },
+      {
+         "name":"name",
+         "type":"string"
+      }
+   ]
+}
diff --git a/data/package-with-missingvalues-993.json b/data/package-with-missingvalues-993.json
new file mode 100644
index 00000000..abc7c307
--- /dev/null
+++ b/data/package-with-missingvalues-993.json
@@ -0,0 +1,24 @@
+{
+   "name":"test-tabulator",
+   "resources":[
+      {
+         "name":"number-two",
+         "path":"table.csv",
+         "schema":{
+            "fields":[
+               {
+                  "name":"id",
+                  "type":"integer"
+               },
+               {
+                  "name":"name",
+                  "type":"string"
+               }
+            ]
+         }
+      }
+   ],
+   "missingValues":[
+      "1"
+   ]
+}
diff --git a/data/resource-with-fields-993.json b/data/resource-with-fields-993.json
new file mode 100644
index 00000000..87467328
--- /dev/null
+++ b/data/resource-with-fields-993.json
@@ -0,0 +1,14 @@
+{
+   "name":"number-two",
+   "path":"table.csv",
+   "fields":[
+      {
+         "name":"id",
+         "type":"integer"
+      },
+      {
+         "name":"name",
+         "type":"string"
+      }
+   ]
+}
\ No newline at end of file
diff --git a/data/resource-with-missingvalues-993.json b/data/resource-with-missingvalues-993.json
new file mode 100644
index 00000000..d900c632
--- /dev/null
+++ b/data/resource-with-missingvalues-993.json
@@ -0,0 +1,19 @@
+{
+   "name":"number-two",
+   "path":"table.csv",
+   "schema":{
+      "fields":[
+         {
+            "name":"id",
+            "type":"integer"
+         },
+         {
+            "name":"name",
+            "type":"string"
+         }
+      ]
+   },
+   "missingValues":[
+      "1"
+   ]
+}
\ No newline at end of file
diff --git a/data/zipped-resources/datapackage.json b/data/zipped-resources/datapackage.json
index e343e82c..bf14a443 100644
--- a/data/zipped-resources/datapackage.json
+++ b/data/zipped-resources/datapackage.json
@@ -66,11 +66,11 @@
                    "description@de":"Beitrag der Finanzquelle zur Deckung des Aufwands der Forschungsstätte (in Schweizer Franken)",
                    "description@fr":"Contribution de la source de financement pour couvrir les dépenses de l'institution de recherche (en francs suisses)"
                 }
-             ]
+             ],
+            "missingValues":[
+               ""
+            ]
           },
-          "missingValues":[
-             ""
-          ],
           "title@de":"Energieforschungsstatistik (EFstat) gemäss Schweizer Klassifikation",
           "title@fr":"Statistique de la recherche énergétique selon classification de recherche CH",
           "format":"csv",
@@ -109,11 +109,11 @@
                    "type":"string",
                    "format":"default"
                 }
-             ]
+             ],
+            "missingValues":[
+               ""
+            ]
           },
-          "missingValues":[
-             ""
-          ],
           "title@de":"Forschungsstätten",
           "title@fr":"Institutions de recherche",
           "format":"csv",
@@ -202,11 +202,11 @@
                    "description@de":"Gruppierung der Finanzquelle",
                    "description@fr":"Groupement de la source de financement"
                 }
-             ]
-          },
-          "missingValues":[
-             ""
-          ],
+             ],
+            "missingValues":[
+               ""
+            ]
+          },          
           "title@de":"Finanzquellen",
           "title@fr":"Sources de financement",
           "format":"csv",
diff --git a/frictionless/package/package.py b/frictionless/package/package.py
index 980f5b04..43e04070 100644
--- a/frictionless/package/package.py
+++ b/frictionless/package/package.py
@@ -704,6 +704,15 @@ class Package(Metadata):
                 dict.__setitem__(self, "resources", resources)
 
     def metadata_validate(self):
+        # Check invalid properties
+        invalid_fields = {
+            "missingValues": "resource.schema.missingValues",
+            "fields": "resource.schema.fields",
+        }
+        for invalid_field, object in invalid_fields.items():
+            if invalid_field in self:
+                note = f'"{invalid_field}" should be set as "{object}" (not "package.{invalid_field}").'
+                yield errors.PackageError(note=note)
 
         # Package
         if self.profile == "data-package":
diff --git a/frictionless/program/validate.py b/frictionless/program/validate.py
index e94751c5..a9d8db9c 100644
--- a/frictionless/program/validate.py
+++ b/frictionless/program/validate.py
@@ -286,7 +286,7 @@ def program_validate(
                 "The document was partially validated because of one of the limits"
             )
             typer.echo("* limit errors")
-            typer.echo("* memomry Limit \n")
+            typer.echo("* memory Limit \n")
         typer.secho(
             str(
                 tabulate(
diff --git a/frictionless/resource/resource.py b/frictionless/resource/resource.py
index 72df027a..24be82c5 100644
--- a/frictionless/resource/resource.py
+++ b/frictionless/resource/resource.py
@@ -1282,6 +1282,16 @@ class Resource(Metadata):
                     raise FrictionlessException(error)
 
     def metadata_validate(self):
+        # Check invalid properties
+        invalid_fields = {
+            "missingValues": "resource.schema.missingValues",
+            "fields": "resource.schema.fields",
+        }
+        for invalid_field, object in invalid_fields.items():
+            if invalid_field in self:
+                note = f'"{invalid_field}" should be set as "{object}" (not "resource.{invalid_field}").'
+                yield errors.ResourceError(note=note)
+
         yield from super().metadata_validate()
 
         # Control/Dialect


Test patch:
diff --git a/tests/package/validate/test_general.py b/tests/package/validate/test_general.py
index 9cd4b61b..25456205 100644
--- a/tests/package/validate/test_general.py
+++ b/tests/package/validate/test_general.py
@@ -345,3 +345,43 @@ def test_validate_package_with_resource_data_is_a_string_issue_977():
     assert report.flatten() == [
         [None, None, None, "package-error"],
     ]
+
+
+def test_validate_package_metadata_errors_with_missing_values_993():
+    package = Package(descriptor="data/package-with-missingvalues-993.json")
+    assert package.metadata_errors[0].code == "package-error"
+    assert (
+        package.metadata_errors[0].note
+        == '"missingValues" should be set as "resource.schema.missingValues" (not "package.missingValues").'
+    )
+
+
+def test_validate_package_metadata_errors_with_fields_993():
+    package = Package(descriptor="data/package-with-fields-993.json")
+    assert package.metadata_errors[0].code == "package-error"
+    assert (
+        package.metadata_errors[0].note
+        == '"fields" should be set as "resource.schema.fields" (not "package.fields").'
+    )
+
+
+def test_validate_package_errors_with_missing_values_993():
+    package = Package(descriptor="data/package-with-missingvalues-993.json")
+    report = package.validate()
+    assert report.flatten(["code", "message"]) == [
+        [
+            "package-error",
+            'The data package has an error: "missingValues" should be set as "resource.schema.missingValues" (not "package.missingValues").',
+        ]
+    ]
+
+
+def test_validate_package_errors_with_fields_993():
+    package = Package(descriptor="data/package-with-fields-993.json")
+    report = package.validate()
+    assert report.flatten(["code", "message"]) == [
+        [
+            "package-error",
+            'The data package has an error: "fields" should be set as "resource.schema.fields" (not "package.fields").',
+        ]
+    ]
diff --git a/tests/resource/validate/test_general.py b/tests/resource/validate/test_general.py
index 7d5e025e..c683eae3 100644
--- a/tests/resource/validate/test_general.py
+++ b/tests/resource/validate/test_general.py
@@ -519,3 +519,43 @@ def test_validate_resource_duplicate_labels_with_sync_schema_issue_910():
             'Duplicate labels in header is not supported with "schema_sync"',
         ],
     ]
+
+
+def test_validate_resource_metadata_errors_with_missing_values_993():
+    resource = Resource("data/resource-with-missingvalues-993.json")
+    assert resource.metadata_errors[0].code == "resource-error"
+    assert (
+        resource.metadata_errors[0].note
+        == '"missingValues" should be set as "resource.schema.missingValues" (not "resource.missingValues").'
+    )
+
+
+def test_validate_resource_metadata_errors_with_fields_993():
+    resource = Resource("data/resource-with-fields-993.json")
+    assert resource.metadata_errors[0].code == "resource-error"
+    assert (
+        resource.metadata_errors[0].note
+        == '"fields" should be set as "resource.schema.fields" (not "resource.fields").'
+    )
+
+
+def test_validate_resource_errors_with_missing_values_993():
+    resource = Resource("data/resource-with-missingvalues-993.json")
+    report = resource.validate()
+    assert report.flatten(["code", "message"]) == [
+        [
+            "resource-error",
+            'The data resource has an error: "missingValues" should be set as "resource.schema.missingValues" (not "resource.missingValues").',
+        ]
+    ]
+
+
+def test_validate_resource_errors_with_fields_993():
+    resource = Resource("data/resource-with-fields-993.json")
+    report = resource.validate()
+    assert report.flatten(["code", "message"]) == [
+        [
+            "resource-error",
+            'The data resource has an error: "fields" should be set as "resource.schema.fields" (not "resource.fields").',
+        ]
+    ]