diff --git a/optimade/models/jsonapi.py b/optimade/models/jsonapi.py
index a6bbd197..80189d34 100644
--- a/optimade/models/jsonapi.py
+++ b/optimade/models/jsonapi.py
@@ -1,5 +1,5 @@
 """This module should reproduce JSON API v1.0 https://jsonapi.org/format/1.0/"""
-from typing import Optional, Set, Union
+from typing import Optional, Set, Union, Any
 from pydantic import BaseModel, UrlStr, Schema, validator
 
 
@@ -180,6 +180,13 @@ class Relationships(BaseModel):
         id
     """
 
+    id: Optional[Any] = Schema(..., description="Not allowed key")
+    type: Optional[Any] = Schema(..., description="Not allowed key")
+
+    @validator("id", "type")
+    def check_illegal_relationships_fields(cls, v):
+        raise AssertionError('"id", "type" MUST NOT be fields under relationships')
+
 
 class ResourceLinks(BaseModel):
     """A Resource Links object"""
@@ -193,16 +200,27 @@ class ResourceLinks(BaseModel):
 class Attributes(BaseModel):
     """
     Members of the attributes object ("attributes\") represent information about the resource object in which it's defined.
-    The keys for Attributes must NOT be:
+    The keys for Attributes MUST NOT be:
         relationships
         links
         id
         type
     """
 
+    relationships: Optional[Any] = Schema(..., description="Not allowed key")
+    links: Optional[Any] = Schema(..., description="Not allowed key")
+    id: Optional[Any] = Schema(..., description="Not allowed key")
+    type: Optional[Any] = Schema(..., description="Not allowed key")
+
     class Config:
         extra = "allow"
 
+    @validator("relationships", "links", "id", "type")
+    def check_illegal_attributes_fields(cls, v):
+        raise ValueError(
+            '"relationships", "links", "id", "type" MUST NOT be fields under attributes'
+        )
+
 
 class Resource(BaseResource):
     """Resource objects appear in a JSON:API document to represent resources."""
diff --git a/optimade/models/structures.py b/optimade/models/structures.py
index c24d2429..31f23b3f 100644
--- a/optimade/models/structures.py
+++ b/optimade/models/structures.py
@@ -621,7 +621,7 @@ class StructureResourceAttributes(EntryResourceAttributes):
 
     @validator("chemical_formula_reduced", "chemical_formula_hill")
     def no_spaces_in_reduced(cls, v):
-        if " " in v:
+        if v and " " in v:
             raise ValueError(f"Spaces are not allowed, you passed: {v}")
         return v
 

server.jsonapi has no patternProperties 
In the current version of pydanitc patternProperties can only be added inside sub-elements of properties. If that is fixed then remove items from optimade.server.models.jsonapi.Attributes and optimade.server.models.jsonapi.Realationships and make it match that new formalism.

Somewhere in the creation of the openapi.json file the patternProperties is lost (likely something is using an old version of pydantic that converts a Dict object to object if the key is not a string).
diff --git a/optimade/models/tests/test_bad_structures.json b/optimade/models/tests/test_bad_structures.json
index 99bcce98..fd9bf7ea 100644
--- a/optimade/models/tests/test_bad_structures.json
+++ b/optimade/models/tests/test_bad_structures.json
@@ -1494,5 +1494,73 @@
     ],
     "structure_features": ["assemblies"],
     "task_id": "mpf_276"
+  },
+  {
+    "_id": {
+      "$oid": "5cfb441f053b174410700d02"
+    },
+    "type": "structure",
+    "links": ["C-H"],
+    "relationships": "single",
+    "id": "some_id",
+    "_mp_chemsys": "Ac",
+    "cartesian_site_positions": [
+      [
+        0.17570227444196573,
+        0.17570227444196573,
+        0.17570227444196573
+      ]
+    ],
+    "dimension_types": [
+      1,
+      1,
+      1
+    ],
+    "elements": [
+      "Ac"
+    ],
+    "elements_ratios": [
+      1.0
+    ],
+    "formula_anonymous": "A",
+    "last_modified": {
+      "$date": "2019-06-08T05:13:37.331Z"
+    },
+    "lattice_vectors": [
+      [
+        1.2503264826932692,
+        0,
+        0
+      ],
+      [
+        0,
+        9.888509716321765,
+        0
+      ],
+      [
+        0,
+        0,
+        0.2972637673241818
+      ]
+    ],
+    "nelements": 1,
+    "nsites": 1,
+    "pretty_formula": "Ac",
+    "species": [
+      {
+        "chemical_symbols": [
+          "Ac"
+        ],
+        "concentration": [
+          1.0
+        ],
+        "name": "Ac"
+      }
+    ],
+    "species_at_sites": [
+      "Ac"
+    ],
+    "structure_features": [],
+    "task_id": "mpf_1"
   }
 ]

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.8
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aiofiles==24.1.0
backports.tarfile==1.2.0
black==24.8.0
certifi==2025.1.31
cffi==1.17.1
cfgv==3.4.0
charset-normalizer==3.4.1
click==8.1.8
cryptography==44.0.2
distlib==0.3.9
dnspython==2.6.1
docutils==0.20.1
email_validator==2.2.0
exceptiongroup==1.2.2
fastapi==0.28.0
filelock==3.16.1
graphene==3.4.3
graphql-core==3.2.6
graphql-relay==3.2.0
h11==0.14.0
id==1.5.0
identify==2.6.1
idna==3.10
importlib_metadata==8.5.0
importlib_resources==6.4.5
iniconfig==2.1.0
invoke==2.2.0
itsdangerous==2.2.0
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jeepney==0.9.0
Jinja2==3.1.6
keyring==25.5.0
lark-parser==0.7.7
markdown-it-py==3.0.0
MarkupSafe==2.1.5
mdurl==0.1.2
mongomock==3.16.0
more-itertools==10.5.0
mypy-extensions==1.0.0
nh3==0.2.21
nodeenv==1.9.1
-e git+https://github.com/Materials-Consortia/optimade-python-tools.git@7aa23880f41492f21f2eb2b6379cf78d807f60a3#egg=optimade
packaging==24.2
pathspec==0.12.1
platformdirs==4.3.6
pluggy==1.5.0
pre-commit==3.5.0
pycparser==2.22
pydantic==0.26
Pygments==2.19.1
pymongo==3.8.0
pytest==8.3.5
python-dateutil==2.9.0.post0
python-multipart==0.0.20
PyYAML==6.0.2
readme_renderer==43.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
SecretStorage==3.3.3
sentinels==1.0.0
six==1.17.0
starlette==0.12.0
tomli==2.2.1
twine==6.1.0
typing_extensions==4.13.0
ujson==5.10.0
urllib3==2.2.3
uvicorn==0.33.0
virtualenv==20.29.3
zipp==3.20.2

name: optimade-python-tools
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=24.2=py38h06a4308_0
  - python=3.8.20=he870216_0
  - readline=8.2=h5eee18b_0
  - setuptools=75.1.0=py38h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.44.0=py38h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aiofiles==24.1.0
      - backports-tarfile==1.2.0
      - black==24.8.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - cfgv==3.4.0
      - charset-normalizer==3.4.1
      - click==8.1.8
      - cryptography==44.0.2
      - distlib==0.3.9
      - dnspython==2.6.1
      - docutils==0.20.1
      - email-validator==2.2.0
      - exceptiongroup==1.2.2
      - fastapi==0.28.0
      - filelock==3.16.1
      - graphene==3.4.3
      - graphql-core==3.2.6
      - graphql-relay==3.2.0
      - h11==0.14.0
      - id==1.5.0
      - identify==2.6.1
      - idna==3.10
      - importlib-metadata==8.5.0
      - importlib-resources==6.4.5
      - iniconfig==2.1.0
      - invoke==2.2.0
      - itsdangerous==2.2.0
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jeepney==0.9.0
      - jinja2==3.1.6
      - keyring==25.5.0
      - lark-parser==0.7.7
      - markdown-it-py==3.0.0
      - markupsafe==2.1.5
      - mdurl==0.1.2
      - mongomock==3.16.0
      - more-itertools==10.5.0
      - mypy-extensions==1.0.0
      - nh3==0.2.21
      - nodeenv==1.9.1
      - packaging==24.2
      - pathspec==0.12.1
      - platformdirs==4.3.6
      - pluggy==1.5.0
      - pre-commit==3.5.0
      - pycparser==2.22
      - pydantic==0.26
      - pygments==2.19.1
      - pymongo==3.8.0
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - python-multipart==0.0.20
      - pyyaml==6.0.2
      - readme-renderer==43.0
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==14.0.0
      - secretstorage==3.3.3
      - sentinels==1.0.0
      - six==1.17.0
      - starlette==0.12.0
      - tomli==2.2.1
      - twine==6.1.0
      - typing-extensions==4.13.0
      - ujson==5.10.0
      - urllib3==2.2.3
      - uvicorn==0.33.0
      - virtualenv==20.29.3
      - zipp==3.20.2
prefix: /opt/conda/envs/optimade-python-tools

optimade/models/tests/test_models.py::TestPydanticValidation::test_bad_structures
optimade/filterparser/tests/test_filterparser.py::ParserTestV0_9_5::test_inputs optimade/filterparser/tests/test_filterparser.py::ParserTestV0_9_5::test_parser_version optimade/filterparser/tests/test_filterparser.py::ParserTestV0_9_5::test_repr optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_empty optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_list_properties optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_number_values optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_operators optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_parser_version optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_precedence optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_properties optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_property_names optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_repr optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_special_cases optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_string_operations optimade/filterparser/tests/test_filterparser.py::ParserTestV0_10_0::test_string_values optimade/filtertransformers/tests/test_mongo.py::TestMongoTransformer::test_empty optimade/filtertransformers/tests/test_mongo.py::TestMongoTransformer::test_number_values optimade/filtertransformers/tests/test_mongo.py::TestMongoTransformer::test_operators optimade/filtertransformers/tests/test_mongo.py::TestMongoTransformer::test_precedence optimade/filtertransformers/tests/test_mongo.py::TestMongoTransformer::test_properties optimade/filtertransformers/tests/test_mongo.py::TestMongoTransformer::test_property_names optimade/filtertransformers/tests/test_mongo.py::TestMongoTransformer::test_simple_comparisons optimade/filtertransformers/tests/test_mongo.py::TestMongoTransformer::test_special_cases optimade/filtertransformers/tests/test_mongo.py::TestMongoTransformer::test_string_values optimade/filtertransformers/tests/test_transformer.py::TestTransformer::test_conjunctions optimade/filtertransformers/tests/test_transformer.py::TestTransformer::test_not optimade/filtertransformers/tests/test_transformer.py::TestTransformer::test_simple_comparisons optimade/models/tests/test_models.py::TestPydanticValidation::test_good_structures optimade/models/tests/test_models.py::test_constrained_list optimade/server/tests/test_server.py::InfoEndpointTests::test_info_endpoint_attributes optimade/server/tests/test_server.py::InfoEndpointTests::test_meta_response optimade/server/tests/test_server.py::InfoEndpointTests::test_serialize_response optimade/server/tests/test_server.py::InfoStructuresEndpointTests::test_info_structures_endpoint_data optimade/server/tests/test_server.py::InfoStructuresEndpointTests::test_meta_response optimade/server/tests/test_server.py::InfoStructuresEndpointTests::test_serialize_response optimade/server/tests/test_server.py::StructuresEndpointTests::test_get_next_responses optimade/server/tests/test_server.py::StructuresEndpointTests::test_meta_response optimade/server/tests/test_server.py::StructuresEndpointTests::test_serialize_response optimade/server/tests/test_server.py::StructuresEndpointTests::test_structures_endpoint_data optimade/server/tests/test_server.py::SingleStructureEndpointTests::test_meta_response optimade/server/tests/test_server.py::SingleStructureEndpointTests::test_serialize_response optimade/server/tests/test_server.py::SingleStructureEndpointTests::test_structures_endpoint_data optimade/server/tests/test_server.py::ServerTestWithValidator::test_with_validator optimade/server/tests/test_server.py::SingleStructureEndpointEmptyTest::test_meta_response optimade/server/tests/test_server.py::SingleStructureEndpointEmptyTest::test_serialize_response optimade/server/tests/test_server.py::SingleStructureEndpointEmptyTest::test_structures_endpoint_data optimade/server/tests/test_server.py::FilterTests::test_aliased_fields optimade/server/tests/test_server.py::FilterTests::test_aliased_is_known optimade/server/tests/test_server.py::FilterTests::test_brackets optimade/server/tests/test_server.py::FilterTests::test_custom_field optimade/server/tests/test_server.py::FilterTests::test_geq optimade/server/tests/test_server.py::FilterTests::test_gt optimade/server/tests/test_server.py::FilterTests::test_gt_none optimade/server/tests/test_server.py::FilterTests::test_id optimade/server/tests/test_server.py::FilterTests::test_is_known optimade/server/tests/test_server.py::FilterTests::test_list_has optimade/server/tests/test_server.py::FilterTests::test_list_has_and optimade/server/tests/test_server.py::FilterTests::test_list_length_basic optimade/server/tests/test_server.py::FilterTests::test_not_or_and_precedence optimade/server/tests/test_server.py::FilterTests::test_page_limit optimade/server/tests/test_server.py::FilterTests::test_string_contains optimade/server/tests/test_server.py::FilterTests::test_string_end optimade/server/tests/test_server.py::FilterTests::test_string_start
optimade/models/structures.py optimade/models/jsonapi.py
optimade/models/structures.py optimade/models/jsonapi.py