Skip to content

Campaign.from_json() fails for campaigns with CompositeSurrogate when JSON was exported from a different process #841

Description

@nihaase

Calling Campaign.from_json() with a JSON containing a CompositeSurrogate raises a TypeError buried in an ExceptionGroup:

TypeError: baybe.surrogates.composite._ReplicationMapping[baybe.surrogates.base.SurrogateProtocol]
is not a module, class, method, or function.

Minimal reproduction

  import json
  from baybe import Campaign

  # Minimal JSON with a CompositeSurrogate (as exported by campaign.to_json())
  json_str = json.dumps({
      "version": "0.15.0",
      "searchspace": {
          "search_space_type": "DISCRETE",
          "parameters": [{"type": "NumericalDiscreteParameter", "name": "x", "values": [1, 2, 3]}],
          "constraints": [],
      },
      "objective": {
          "type": "SingleTargetObjective",
          "target": {"type": "NumericalTarget", "name": "y"},
      },
      "recommender": {
          "type": "TwoPhaseMetaRecommender",
          "recommender": {
              "type": "BotorchRecommender",
              "surrogate_model": {
                  "type": "CompositeSurrogate",
                  "surrogates": {
                      "type": "_ReplicationMapping",
                      "template": {"type": "GaussianProcessSurrogate"},
                  },
              },
          },
      },
  })

  Campaign.from_json(json_str)  # raises

Note: a same-process round-trip (campaign.to_json() followed by Campaign.from_json()) does not reproduce the bug, because to_json() accidentally resolves the type annotations as a side effect.

Root cause

composite.py uses from future import annotations, which stores all type annotations as lazy strings. _ReplicationMapping.template stores its type as the string "_T" instead of the resolved TypeVar.

When cattrs generates a structure hook for _ReplicationMapping[SurrogateProtocol] (a parameterized generic), it detects the unresolved string annotations and calls attrs.resolve_types(_ReplicationMapping[SurrogateProtocol]). Python's typing.get_type_hints() rejects parameterized generics — only plain classes are valid — and raises the TypeError.

The issue originates in _structure_surrogate_getter, which creates _ReplicationMapping[SurrogateProtocol] and hands it directly to the converter without a registered structure hook:

def _structure_surrogate_getter(obj: dict, _) -> _SurrogateGetter:
      container_type = _get_surrogate_getter_type(obj.pop(_TYPE_FIELD))
      return converter.structure(obj, container_type)  # no hook registered for this type

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions