nlohmann / json

JSON for Modern C++
https://json.nlohmann.me
MIT License
41.3k stars 6.58k forks source link

Behavioral change of serializers in 3.11.3. Name lookup related. #4320

Open TobiSchluter opened 3 months ago

TobiSchluter commented 3 months ago

Description

This is a behavioral change in nlohmann::json 3.11.3 compared to previous versions. Old code of mine started behaving differently.

Output of the test program changed from: nlohmann::json 3.11.2 (and older versions) {"x":1.0,"y":2.0,"z":3.0} {"x":1.0,"y":2.0,"z":3.0}

To: nlohmann::json 3.11.3 [1.0,2.0,3.0] {"x":1.0,"y":2.0,"z":3.0}

The issue appears to be related to the lookup of to_json converters. Notice that the old behavior can be triggered by adding the namespace prefix to the to_json call in N::to_json.

Reproduction steps

Run the code reproduced below. Results will differ between 3.11.2 and 3.11.3.

It needs the Eigen library, version 3.4.0. What appears to happen is that the custom vector type is recognized as "array compatible" and then output as such.

Expected vs. actual results

The output should be as in older versions of nlohmann::json: {"x":1.0,"y":2.0,"z":3.0} {"x":1.0,"y":2.0,"z":3.0} instead we get [1.0,2.0,3.0] {"x":1.0,"y":2.0,"z":3.0} i.e. my serializer is bypassed.

Minimal code example

#include <Eigen/Dense>
#include <iostream>
#include <nlohmann/json.hpp>

// Types for data exchange.
namespace N {
    struct Vector {
        double x, y, z;
    };
}

namespace L {
    using Vec3d = Eigen::Vector3d;
}

namespace N {
    // I don't recall why my code goes through this intermediate type, but this is likely related to the change in behavior.
    class Vec3d : public L::Vec3d {
    public:
        explicit Vec3d(const L::Vec3d& v) noexcept
            : L::Vec3d(v)
        {}
        Vec3d(const Vector& v) noexcept
            : L::Vec3d(v.x, v.y, v.z)
        {}
        using L::Vec3d::Vec3d;
    };
    inline Vec3d vToEigen(const Vector& v) noexcept { return Vec3d(v); }
}

namespace Eigen {
    inline void to_json(nlohmann::json& j, const L::Vec3d& v) noexcept
    {
        j = { { "x", v.x() },{ "y", v.y() },{ "z", v.z() } };
    }
}

namespace N {
    // Lookup of to_json changed.  Previously this function behaved the same as ...
    inline void to_json(nlohmann::json& j, const Vector& v) noexcept {
        to_json(j, vToEigen(v));
    }
    // ... this function
    inline void to_json_force(nlohmann::json& j, const Vector& v) noexcept {
        Eigen::to_json(j, vToEigen(v));
    }
}

int main()
{
    N::Vector vec{ 1.0, 2.0, 3.0 };

    // Both outputs were the same until and including nlohmann::json 3.11.2
    nlohmann::json json;
    N::to_json(json, vec);
    std::cout << json.dump() << "\n";

    nlohmann::json json_forced;
    N::to_json_force(json_forced, vec);
    std::cout << json_forced.dump() << "\n";
}

Error messages

No response

Compiler and operating system

msvc 2022 (latest) on Windows x86-64 and gcc 13 on Linux-x64

Library version

3.11.3

Validation

TobiSchluter commented 2 months ago

This also happens with the trunk version at godbolt: https://godbolt.org/z/3neMTxvYc