ArduinoJson v7 deserialization problem

I'm getting data from my Google calendar in JSON format. Most of the time all goes well. But occasionally the deserialization produces erratic results. I tried with the payload as String or as c-string (const char *), but that didn't make any difference. It looks like the deserialize function is missing the terminating curly bracket and then picks up left-overs from a previous run. Which is odd, since I clear the String and the Json Document before I do the http GET().
When I copy/paste the payload string in the Json Assistant everything seems correct.
The problem only occurs when I get a shorter string as payload after getting a longer string (i.e. I first ask the calendar data for the coming 100 days and then again for the coming 20 days).

Here is part of my code:

        // Now get the calendar data from the Google calendar

    Serial.println("Getting Google Recycle calendar data...");

    http.begin(urlString);
    http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);                     // Google script always redirects to Google calendar API

    String payload;
    payload.clear();
    
    jsonDoc.clear();
    DeserializationError err;
    
    int httpResponseCode = http.GET();                                          // execute the GET request and wait for the response

    if (httpResponseCode > 0)                                                   //Check the http result code (https://http.dev/status)
    { 
      Serial.println("checking response...");
      payload = (http.getString());
      
      Serial.print("HTTP Response code: ");
      Serial.println(httpResponseCode);
      Serial.printf("Length of payload string: %lu\n", payload.length());
      Serial.print("Payload as String : ");
      Serial.println(payload);
      Serial.printf("Length of payload char[]: %lu\n", strlen(payload.c_str()));
      Serial.print("Payload as char [] : ");
      Serial.println(payload.c_str());

      err = deserializeJson(jsonDoc, payload);

      if(err)
      {
        Serial.print("deserializeJson() failed: ");
        Serial.println(err.c_str());
      }
      else
      {
        if(jsonDoc["data"].is<JsonArray>())
        {
          JsonArray j_dataArr = jsonDoc["data"].as<JsonArray>();
          const char * timeString;

          const char dateAsString [15] = {'\0'};
          uint8_t dateLen = 0;
          uint8_t eventIndex = 0;

          eventCount = 0;

          for(auto event : events)
          {
            event.type = "";
          }

          for(auto j_item : j_dataArr)
          {
            events[eventIndex].type = j_item["TYPE"].as<const char *>();          // event type
            timeString = j_item["DATE"].as<const char *>();                       // event time
            strptime(timeString, "%Y-%m-%dT%H:%M:%S", &events[eventIndex].calschedule);
            ++eventCount;

            if(++eventIndex >= sizeof(events)/sizeof(gCalEvent_t))                // if event table full
              break;                                                              // stop iteration
          }
          
          eventIndex = 1;
          for(auto event : events)
          {
            if((event.type == NULL) || strlen(event.type) == 0)
              break;

            Serial.print("Event #");
            Serial.print(eventIndex++);
            Serial.print(" - TYPE: ");
            Serial.print(event.type);
            Serial.print(", Date: ");
            Serial.println(&event.calschedule, "%A, %B %d %Y");
          }
        }
        else if(jsonDoc["data"].is<const char *>())                                 // if not an array it must be a c-string
        {
          const char * GCdata = jsonDoc["data"].as<const char *>();
          Serial.printf("Google Cal data = %s\n", GCdata);
        }
        else
        {
          Serial.println("Google Data don't make sense...");
        }
      }

It's not a killer problem, since the normal use case is to request only the calender data for the next day, but I'm puzzled why I'm getting this strange misbehaviour.
Hint: deserialization typically fails (does not report an error) if the lenght of the string is between 200 and 400 characters.

The results look like this:

This one is OK. I requested data for the coming 50 days. The payload contains 663 characters and deserialization produces a correct result. Note: I only keep the first 10 results to conserve memory
16:12:38.588 -> Getting Google Recycle calendar data...

16:12:44.019 -> checking response...

16:12:44.127 -> HTTP Response code: 200

16:12:44.127 -> Length of payload string: 663

16:12:44.127 -> Payload as String : {"status":"success","data":[{"TYPE":"RA","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"P/K","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-04-24T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-24T05:00:00.000Z"},{"TYPE":"GLAS","DATE":"2024-05-03T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-05-03T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-05-08T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-05-08T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-05-15T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-05-22T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-05-22T05:00:00.000Z"}]}

16:12:44.128 -> Length of payload char[]: 663

16:12:44.128 -> Payload as char [] : {"status":"success","data":[{"TYPE":"RA","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"P/K","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-04-24T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-24T05:00:00.000Z"},{"TYPE":"GLAS","DATE":"2024-05-03T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-05-03T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-05-08T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-05-08T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-05-15T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-05-22T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-05-22T05:00:00.000Z"}]}

16:12:44.179 -> Event #1 - TYPE: RA, Date: Wednesday, April 10 2024

16:12:44.179 -> Event #2 - TYPE: PMD, Date: Wednesday, April 10 2024

16:12:44.179 -> Event #3 - TYPE: P/K, Date: Wednesday, April 17 2024

16:12:44.210 -> Event #4 - TYPE: GFT, Date: Wednesday, April 17 2024

16:12:44.210 -> Event #5 - TYPE: RA, Date: Wednesday, April 24 2024

16:12:44.210 -> Event #6 - TYPE: PMD, Date: Wednesday, April 24 2024

16:12:44.210 -> Event #7 - TYPE: GLAS, Date: Friday, May 03 2024

16:12:44.210 -> Event #8 - TYPE: GFT, Date: Friday, May 03 2024

16:12:44.210 -> Event #9 - TYPE: RA, Date: Wednesday, May 08 2024

16:12:44.210 -> Event #10 - TYPE: PMD, Date: Wednesday, May 08 2024

Next, I did a request for the coming 20 days...

16:12:38.588 -> Getting Google Recycle calendar data...

16:12:44.019 -> checking response...

16:12:44.127 -> HTTP Response code: 200

16:12:44.127 -> Length of payload string: 663

16:12:44.127 -> Payload as String : {"status":"success","data":[{"TYPE":"RA","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"P/K","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-04-24T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-24T05:00:00.000Z"},{"TYPE":"GLAS","DATE":"2024-05-03T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-05-03T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-05-08T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-05-08T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-05-15T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-05-22T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-05-22T05:00:00.000Z"}]}

16:12:44.128 -> Length of payload char[]: 663

16:12:44.128 -> Payload as char [] : {"status":"success","data":[{"TYPE":"RA","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"P/K","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-04-24T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-24T05:00:00.000Z"},{"TYPE":"GLAS","DATE":"2024-05-03T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-05-03T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-05-08T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-05-08T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-05-15T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-05-22T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-05-22T05:00:00.000Z"}]}

16:12:44.179 -> Event #1 - TYPE: RA, Date: Wednesday, April 10 2024

16:12:44.179 -> Event #2 - TYPE: PMD, Date: Wednesday, April 10 2024

16:12:44.179 -> Event #3 - TYPE: P/K, Date: Wednesday, April 17 2024

16:12:44.210 -> Event #4 - TYPE: GFT, Date: Wednesday, April 17 2024

16:12:44.210 -> Event #5 - TYPE: RA, Date: Wednesday, April 24 2024

16:12:44.210 -> Event #6 - TYPE: PMD, Date: Wednesday, April 24 2024

16:12:44.210 -> Event #7 - TYPE: GLAS, Date: Friday, May 03 2024

16:12:44.210 -> Event #8 - TYPE: GFT, Date: Friday, May 03 2024

16:12:44.210 -> Event #9 - TYPE: RA, Date: Wednesday, May 08 2024

16:12:44.210 -> Event #10 - TYPE: PMD, Date: Wednesday, May 08 2024

16:12:50.179 -> Getting input...

16:12:50.179 -> Command = google20

16:12:50.179 -> String length = 8

16:12:50.179 -> timeSpanAsChar = 20

16:12:50.179 -> constructed URL = https://script.google.com/macros/s/AKfycbzt-p2UuSj-6cUdxoxOxNrCtvoCieO1Y4N6r8IxYpytoeGlly2O9XJLZo7I482YhjyytQ/exec?timeSpan=20

16:12:50.217 -> Getting Google Recycle calendar data...

16:12:56.442 -> checking response...

16:12:56.442 -> HTTP Response code: 200

16:12:56.442 -> Length of payload string: 321

16:12:56.442 -> Payload as String : {"status":"success","data":[{"TYPE":"RA","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"P/K","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-04-24T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-24T05:00:00.000Z"}]}

16:12:56.522 -> Length of payload char[]: 321

16:12:56.522 -> Payload as char [] : {"status":"success","data":[{"TYPE":"RA","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-10T05:00:00.000Z"},{"TYPE":"P/K","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"GFT","DATE":"2024-04-17T05:00:00.000Z"},{"TYPE":"RA","DATE":"2024-04-24T05:00:00.000Z"},{"TYPE":"PMD","DATE":"2024-04-24T05:00:00.000Z"}]}

16:12:56.529 -> Event #1 - TYPE: RA, Date: Wednesday, April 10 2024

16:12:56.529 -> Event #2 - TYPE: PMD, Date: Wednesday, April 10 2024

16:12:56.529 -> Event #3 - TYPE: P/K, Date: Wednesday, April 17 2024

16:12:56.529 -> Event #4 - TYPE: GFT, Date: Wednesday, April 17 2024

16:12:56.529 -> Event #5 - TYPE: RA, Date: Wednesday, April 24 2024

16:12:56.529 -> Event #6 - TYPE: PMD, Date: Wednesday, April 24 2024

16:12:56.550 -> Event #7 - TYPE: (, Date: Friday, May 03 2024

16:12:56.550 -> Event #8 - TYPE: xV��, Date: Friday, May 03 2024

16:12:56.550 -> Event #9 - TYPE: �  , Date: Wednesday, May 08 2024

16:12:56.550 -> Event #10 - TYPE: 9000042Z 280128000042Z0G1 0 UUS1"0 U

16:12:56.550 -> Google Trust Services LLC10U GTS Root R10�"0  *�H�� , Date: Wednesday, May 08 2024

It appears that the data for the first 20 days are correct, but deserialization produces additional results (actually garbage) which are not contained in the payload string. Looks like deserialization is not starting from a clean slate?

First things first: what Arduino board are you using?

If it's a UNO you should avoid "String" variables like the plague because it brings often to unpredictable results, up to complete MCU lock. So in this case get rid of the "String payload;" and any other (maybe including "jsonDoc" we don't know about). Converting the String payload into C string doesn't make any change to that behaviour, especiall' if "temporary" (e.g. it is repeadetly created and discarded). When possible, use only pre-allocated char arrays (with a size greater than the longest string you could receive/use).

regarding

the doc states

You don’t need to call JsonDocument::clear():

These functions already clear the memory pool, so you don’t need to call JsonDocument::clear().

Every reference (JsonArray, JsonObject, or JsonVariant) acquired before calling clear() is invalidated.


as you create a local payload String instance, it's empty at that point

so no need to call payload.clear(); either esp. as you initialise it fully later with payload = (http.getString()); (the parentheses are not needed)

what do you see from the Serial.print ?

I'm using ESP 32 DEVKIT V1. Ram shouldn't be the problem. I know that using String is a PITA on Arduino. But the String is on the stack and I'm assuming that RAM is re-alocated every time I call the function? The Jsondoc is on the heap and according to the documentation allocates RAM dynamically based on the needs. Using a pre-allocated array is not a realistic option as I cannot etimate how much data I will get from the GET request.

@J-M-L I'm aware that I don't need to initialize those variables. But considering the erratic results I was trying if this would make a difference. But sadly, it doesn't. But the redundant clearing shouldn't harm...?
The serial print is in the post. Anything missing that could help?

PS: what happens if I get more data that the size of my buffer? Will it overflow or will http.getString() produce an incomplete result?

Whew, ok! So forget what I said, including the "pre-allocated" string (and forget the useless c_str conversion). Anyway I always prefere using global or static variables, to avoid any memory management, so I'd put the "String payload;" at the top, and just use it as needed.

PS: please, remember to always give all the information about the environment, including (starting from) the board!

no it should not

Missed that the first time.
Seems you get a correctly formed JSON so I would look into any buffer management you have in the code and search for a possible overflow that could corrupt other data or a global index that you have not reset

The immediate problem is here

for(auto event : events)
{
  event.type = "";
}

Try it with one additional symbol

auto &event

You want the variable inside the loop to be a reference to the element in array, so that you can clear the type. Without the &, the variable is a copy of the array element, and modifying a short-lived copy here has no effect. So you see "leftovers".

Which leads to the other problem.

events[eventIndex].type = j_item["TYPE"].as<const char *>();

This references the array element by its index, and is therefore modifying the "actual" element's type. But it is assigning a pointer to a C-string in memory claimed by the JSON document. Once that document is cleared and/or something else is deserialized in its place, that memory is freed and can be used by something else. Those pointers -- which have not changed -- now point to whatever that is; and so as part of the leftovers, you may see "garbage".

If the first problem ("leftovers") is fixed, then this is not an issue if the JsonDocument that last populated the array is not cleared and sticks around as long as the elements in the events array. But if the array has to outlive the JSON, then you have to do something else, with the type, like

  • Use String instead, which is designed to store short strings "inside itself" and not lead to a heap fragmentation
  • Similarly, have a small char array, and copy the bytes over
  • If the TYPEs are known, have them defined in a static or global array, and after matching, store that index instead

You nailed it. Using 'auto &event' fixed the problem. Thank you so much.
Concerning the second issue:
I always assumed that j_item["TYPE"].as<const char *>(); copies the chars from the JSON doc to the destination char[]. Apparently it doesn't.
I now use an intermediate char * which points to the JSON array member and then copy those characters to my events.type c-string using strncpy.

you ask to get them as a const char * meaning you can't change the char, so it's not useful to make a copy

I've never tried, but if you were to ask .as<char *>, may be you'd get a copy

The const-ness has nothing to do with it. Even when you cast the result to <char *> the data would still reside inside the jsonDoc. There's a reason why the author of the library did put effort in protecting the data inside the jsonDoc: don't mess with the internals.

I made 2 mistakes:

  1. I used the pointer produced by the function j_item["TYPE"].as<const char *>() assuming that it would return the data of the char[].
  2. I didn't initialize the global variable where I saved that pointer, which explains the 'garbage' which I saw when doing a second GET request which returned a lower number of items. What I saw were the old pointers from the previous GET request pointing to random data in RAM.
    The solution proposed by kenb4 did the trick: ectract the data from the jsonDoc by making a copy to a char[] in my global variable (and init the global variable before doing that).
    Thanks for trying.

My global variable where I storre the results now looks like this:

typedef struct
{
  char type[5];                                                                 // type of the event
  struct tm calschedule;                                                        // time and date of the event
  bool notifyUser;                                                              // true if user needs to be notified
} gCalEvent_t; 

And the code to extract the data from the jsonDoc:

          for(auto j_item : j_dataArr)                                          // iterate over JsonArray
          {
            // get the TYPE values and save them in events[index].type;

            strncpy(events[index].type, j_item["TYPE"].as<const char *>(), sizeof(events[index].type));

            // get time and date and save them as tm struct in events.calschedule

            strptime(j_item["DATE"].as<const char *>(), "%Y-%m-%dT%H:%M:%S", &events[index].calschedule);

            if(++index >= ARRAY_SIZE(events))                                   // if event table full
              break;                                                            // stop iteration
          }

It's not a cast I was talking about.

It's a member function implemented in the library. It could have provided a newly allocated buffer with a copy of the content like when you ask .as<String> or a value when you ask .as<int> - that's what I was wondering about.

Looking at the documentation there is no .as<char *> only .as<const char *> and in which case it makes sens that it points directly to the internal since it's const, it should not be messed with.

It also makes sense that there is no .as<char *> as it's not a great practice to have a function allocate a buffer and forget about its responsibility to free it.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.