"invalid type: map, expected a sequence" when deserializing a nested JSON structure with Serde

zep picture zep · Feb 3, 2018 · Viewed 10.6k times · Source

I am trying to poll the GitHub API for issues and print them out. To do so, I need to deserialize a nested JSON structure that I receive from a cURL GET request.

I am trying to get the url for all the objects in the items array:

{
 "total_count": 4905,
 "incomplete_results": false,
 "items": [
    {
     "url": "https://api.github.com/repos/servo/saltfs/issues/789",
     "repository_url": "https://api.github.com/repos/servo/saltfs",
     "labels_url": 
   "https://api.github.com/repos/servo/saltfs/issues/789/labels{/name}",
  "comments_url": "https://api.github.com/repos/servo/saltfs/issues/789/comments",
  "events_url": "https://api.github.com/repos/servo/saltfs/issues/789/events",
  "html_url": "https://github.com/servo/saltfs/issues/789",
  "id": 293260512,
  "number": 789,
  "title": "Stop setting $CARGO_HOME to its default value",
  "user": {
    "login": "SimonSapin",
    "id": 291359,
    "avatar_url": "https://avatars0.githubusercontent.com/u/291359?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/SimonSapin",
    "html_url": "https://github.com/SimonSapin",
    "followers_url": "https://api.github.com/users/SimonSapin/followers",
    "following_url": "https://api.github.com/users/SimonSapin/following{/other_user}",
    "gists_url": "https://api.github.com/users/SimonSapin/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/SimonSapin/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/SimonSapin/subscriptions",
    "organizations_url": "https://api.github.com/users/SimonSapin/orgs",
    "repos_url": "https://api.github.com/users/SimonSapin/repos",
    "events_url": "https://api.github.com/users/SimonSapin/events{/privacy}",
    "received_events_url": "https://api.github.com/users/SimonSapin/received_events",
    "type": "User",
    "site_admin": false
  },
  "labels": [
    {
      "id": 341722396,
      "url": "https://api.github.com/repos/servo/saltfs/labels/E-easy",
      "name": "E-easy",
      "color": "02e10c",
      "default": false
    }
  ],
  "state": "open",
  "locked": false,
  "assignee": null,
  "assignees": [

  ],
  "milestone": null,
  "comments": 0,
  "created_at": "2018-01-31T18:16:09Z",
  "updated_at": "2018-01-31T18:16:49Z",
  "closed_at": null,
  "author_association": "MEMBER",
  "body": "In `buildbot/master/files/config/environments.py` we set `CARGO_HOME` to Cargo’s default value. Now that `mach` does not set it (since https://github.com/servo/servo/pull/19395), this has no effect. We can remove these lines.",
  "score": 1.0
},
{
  "url": "https://api.github.com/repos/servo/servo/issues/19916",
  "repository_url": "https://api.github.com/repos/servo/servo",
  "labels_url": "https://api.github.com/repos/servo/servo/issues/19916/labels{/name}",
  "comments_url": "https://api.github.com/repos/servo/servo/issues/19916/comments",
  "events_url": "https://api.github.com/repos/servo/servo/issues/19916/events",
  "html_url": "https://github.com/servo/servo/issues/19916",
  "id": 293237180,
  "number": 19916,
  "title": "Use a macro to create null-terminated C strings",
  "user": {
    "login": "jdm",
    "id": 27658,
    "avatar_url": "https://avatars1.githubusercontent.com/u/27658?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/jdm",
    "html_url": "https://github.com/jdm",
    "followers_url": "https://api.github.com/users/jdm/followers",
    "following_url": "https://api.github.com/users/jdm/following{/other_user}",
    "gists_url": "https://api.github.com/users/jdm/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/jdm/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/jdm/subscriptions",
    "organizations_url": "https://api.github.com/users/jdm/orgs",
    "repos_url": "https://api.github.com/users/jdm/repos",
    "events_url": "https://api.github.com/users/jdm/events{/privacy}",
    "received_events_url": "https://api.github.com/users/jdm/received_events",
    "type": "User",
    "site_admin": false
  },
  "labels": [
    {
      "id": 89384911,
      "url": "https://api.github.com/repos/servo/servo/labels/C-assigned",
      "name": "C-assigned",
      "color": "02d7e1",
      "default": false
    },
    {
      "id": 15997664,
      "url": "https://api.github.com/repos/servo/servo/labels/E-easy",
      "name": "E-easy",
      "color": "02e10c",
      "default": false
    },
    {
      "id": 135307111,
      "url": "https://api.github.com/repos/servo/servo/labels/I-cleanup",
      "name": "I-cleanup",
      "color": "e11d21",
      "default": false
    }
  ],
  "state": "open",
  "locked": false,
  "assignee": null,
  "assignees": [

  ],
  "milestone": null,
  "comments": 3,
  "created_at": "2018-01-31T17:04:06Z",
  "updated_at": "2018-01-31T22:03:56Z",
  "closed_at": null,
  "author_association": "MEMBER",
  "body": "When we write them by hand (eg. `b\"some string\\0\"`), we invariably get them wrong in ways that are tricky to notice (https://github.com/servo/servo/pull/19915). We should use a macro like this instead:\r\n```rust\r\nmacro_rules! c_str {\r\n    ($str:expr) => {\r\n        concat!($str, \"\\0\").as_bytes()\r\n    }\r\n}\r\n```\r\nThis would allow us to write code like `(c_str!(\"PEParseDeclarationDeclExpected\"), Action::Skip)` instead of https://github.com/emilio/servo/blob/d82c54bd3033cc3277ebeb4854739bebe4e20f2f/ports/geckolib/error_reporter.rs#L237. We should be able to clean up all of the uses in that file.\r\n\r\nNo need to run any automated tests; if it builds with `./mach build-geckolib`, then it's good enough for a pull request.",
  "score": 1.0
}
]}

My request function makes a cURL request and receives the above JSON. I then use serde_json to deserialize the JSON

main.rs

extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

mod engine;
mod server;
use engine::request;
use std::string::String;
use self::serde_json::{Error, Value};

#[derive(Serialize, Deserialize)]
struct obj {
    items: Vec<String>,
}

fn main() {
    let output_jn: String = request(
        "https://api.github.com/search/issues?q=is:issue+label:e-easy",
    ).to_string(); //gets json structure as string
    let json: obj = serde_json::from_str(&output_jn).unwrap();

    for elem in json.iter() {
        println!("{:?}", elem);
    }
}

I get the following error message

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: 
ErrorImpl { code: Message("invalid type: map, expected a sequence"), 
line: 1, column: 0 }', libcore/result.rs:945:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

I'm certain I'm making a stupid mistake in deserializing my JSON structure, I've tried a number of permutations and combinations but I couldn't get anything to work.

Answer

dtolnay picture dtolnay · Feb 25, 2018

Have a look at this part of your JSON input data:

{
  ...
  "items": [
    {
      ...
      "title": "Stop setting $CARGO_HOME to its default value",
      ...
    }
  ]
}
  • The top-level data structure is a JSON map, so in Rust this will be represented as a struct. I will use your name Obj.
  • The top-level JSON map has a key called "items" so in Rust this will be a field items inside the Obj struct.
  • The value of "items" in the map is a JSON array, so in Rust let's use a Vec.
  • Each element in the JSON array is a JSON map so in Rust we need a struct for those. We can call it Issue.
  • Each issue has a JSON key called "title" so this will be a field title inside the Issue struct.
  • The value of "title" is a JSON string so we can use Rust's String type for the field.

#[derive(Deserialize, Debug)]
struct Obj {
    items: Vec<Issue>,
}

#[derive(Deserialize, Debug)]
struct Issue {
    title: String,
}

fn main() {
    let j = /* get the JSON data */;

    let issues = serde_json::from_str::<Obj>(j).unwrap();
    for i in issues.items {
        println!("{:#?}", i);
    }
}