nlohmann / json

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

Program crashes with ordered_json, but works fine with json #4279

Open vircon32 opened 5 months ago

vircon32 commented 5 months ago

Description

The same program (code in case 1) produces the expected result using regular nlohmann::json, but crashes using nlohmann::ordered_json. A simple variant of this program (code in case 2) produces the expected result using json, but a different result with ordered_json.

Reproduction steps

Consider this input json file ("test.json"):

{
  "number": 14,
  "structure": {
    "field1": 1,
    "field2": 2
  }
}

The 2 small programs listed into "Minimal code example" will work fine and produce the expected results when using my_json = nlohmann::json. However, if we change the type to nlohmann::ordered_json, the first program will crash and the second program will not add new_field as expected.

Expected vs. actual results

Expected result for case 1:

{
  "number": 14,
  "my_structures": {
    "structure": {
      "field1": 1
    }
  }
}

Expected result for case 2:

{
  "number": 14,
  "my_structures": {
    "structure": {
      "field1": 1,
      "field2": 2,
      "new_field": "new"
    }
  }
}

Minimal code example

// CASE 1: removing an old field
using my_json = nlohmann::json;

int main()
{
    std::ifstream InputStream( "test.json" );
    my_json Root = my_json::parse( InputStream );

    // create new level at root
    my_json& MyStructures = Root[ "my_structures" ];

    // move structure into that new level
    MyStructures[ "structure" ] = Root[ "structure" ];
    Root.erase( "structure" );

    // add new structure field
    MyStructures[ "structure" ].erase( "field2" );

    std::ofstream OutputStream( "converted.json" );
    OutputStream << std::setw( 2 ) << Root << std::endl;    
}

// CASE 2: adding a new field
using my_json = nlohmann::json;

int main()
{
    std::ifstream InputStream( "test.json" );
    my_json Root = my_json::parse( InputStream );

    // create new level at root
    my_json& MyStructures = Root[ "my_structures" ];

    // move structure into that new level
    MyStructures[ "structure" ] = Root[ "structure" ];
    Root.erase( "structure" );

    // add new structure field
    MyStructures[ "structure" ][ "new_field" ] = "new";

    std::ofstream OutputStream( "converted.json" );
    OutputStream << std::setw( 2 ) << Root << std::endl;    
}

Error messages

I am using the library as a single include, and for the crashing case an exception is being thrown at this point (line 21878):

  private:
    template < typename KeyType, detail::enable_if_t <
                   detail::has_erase_with_key_type<basic_json_t, KeyType>::value, int > = 0 >
    size_type erase_internal(KeyType && key)
    {
        // this erase only works for objects
        if (JSON_HEDLEY_UNLIKELY(!is_object()))
        {
    ----->   JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this));
        }

        return m_data.m_value.object->erase(std::forward<KeyType>(key));
    }

The exception message is: [json.exception.type_error.307] cannot use erase() with null

Compiler and operating system

Windows 10, Microsoft Visual Studio Professional 2019 Version 16.11.24

Library version

Github at commit 7efe875495a3ed7d805ddbb01af0c7725f50c88b

Validation

vircon32 commented 5 months ago

Update: I have verified that the crash also happens when I use json.hpp from the latest release (version 3.11.3).

gregmarr commented 5 months ago
// create new level at root
my_json& MyStructures = Root[ "my_structures" ];

// move structure into that new level
MyStructures[ "structure" ] = Root[ "structure" ];
Root.erase( "structure" );

// add new structure field
MyStructures[ "structure" ].erase( "field2" );

You can't keep references into the ordered_json across modifications to the fields of the object, as it uses a vector internally. Try this:

// create new level at root and 
// move structure into that new level
Root[ "my_structures" ][ "structure" ] = Root[ "structure" ];
Root.erase( "structure" );

// add new structure field
Root[ "my_structures" ][ "structure" ].erase( "field2" );
vircon32 commented 5 months ago

Thank you gregmarr, it does work this way. I didn't know of this limitation. Does the documentation mention this? If so, please excuse me.

gregmarr commented 5 months ago

I don't think there is a section in iterator/pointer/reference invalidation. It is mentioned in the code at https://github.com/nlohmann/json/blob/develop/include/nlohmann/json.hpp#L768 and was the cause of https://github.com/nlohmann/json/issues/2962 fixed by https://github.com/nlohmann/json/pull/2963

There is a "todo" section here: https://github.com/nlohmann/json/blob/0457de21cffb298c22b629e538036bfeb96130b7/docs/mkdocs/docs/api/basic_json/index.md?plain=1#L43

nlohmann commented 5 months ago

Thanks @gregmarr. I marked this as a documentation issue.