Clap derive arg to parse json array of objects into Vec<MyStruct>

I am struggling to use clap's derive syntax to define an argument that takes a JSON array as a value and parses it into a Vec

Here's how I want to pass the argument in my CLI:

myapp --my-structs '[{"name":"josh"}, {"name:"shelly"}]'

My desired result would be like:

args == Args {
    my_structs: [
        MyStruct { name: "josh" },
        MyStruct { name: "shelly" },
    ]
}

Here's my code:

use clap::{Command, CommandFactory, Parser, ValueHint};
use serde::Deserialize;
use std::str::FromStr;

#[derive(Deserialize)]
struct MyStruct {
    name: String,
}

impl FromStr for MyStruct {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let res: MyStruct =
            serde_json::from_str(s).map_err(|e| format!("error parsing my struct: {}", e))?;
        Ok(res)
    }
}


#[derive(Parser, Debug)]
pub struct Args {
    #[arg(
        long,
        value_name = "MY_STRUCTS",
        help = "an array of MyStructs",
    )]
    my_structs: Vec<MyStruct>
}


// The primary function that runs the program
pub fn run() {
    let mut args: Args = Args::parse();
}

The error i get says:

error: invalid value '[{"name":"josh"},{"name":"shelly"}]' for '--error-rules <ERROR_RULES>': error parsing my struct: invalid type: map, expected a string at line 1 column 1

Thanks so much in advance to any clap veterans who can point me in the right direction!

1 Like

I'm no clap veteran, but I've run into a similar problem before!

I think the main problem is a misunderstanding on how Vec<MyStruct> gets used by clap.
This signals to clap: "this option can be specified multiple times":
myapp --my-structs '{"name":"josh"}' --my-structs '{"name:"shelly"}'
It doesn't signal: "multiple values can be provided to this option at once".
myapp --my-structs '[{"name":"josh"}, {"name:"shelly"}]'

See: Rust Playground for a working example of what I mean.


But the real question, is how to get Clap to support the exact syntax you want!
Like I said, I'm not a clap veteran, so hopefully someone else can chime in with the magic attribute to use here! But I see two ways of getting close to what you want.

Option 1

Introducing a thin wrapper around Vec<MyStruct> and using that type in your Clap struct:

struct MyStructs(Vec<MyStruct>);
#[derive(Parser, Debug)]
pub struct Args {
    #[arg(
        long,
        value_name = "MY_STRUCTS",
        help = "an array of MyStructs",
    )]
-    my_structs: Vec<MyStruct>
+    my_structs: MyStructs
}

See a working example here: Rust Playground

This will exactly parse the syntax you want, but it adds a layer of indirection to your Args now.
This works because clap will no longer see a Vec type in it's arguments.

Option 2

Using the num_args = 0.. modifier to tell clap to let you write 0 or more arguments in a sequence:

#[derive(Parser, Debug)]
pub struct Args {
    #[arg(
        long,
        value_name = "MY_STRUCTS",
        help = "an array of MyStructs",
+       num_args = 0..,
    )]
    my_structs: Vec<MyStruct>
}

this would let you pass in things like:
myapp --my-structs '{"name":"josh"}' '{"name:"shelly"}'
Which is close to what you want, but leaves the type of your Args untouched.

2 Likes

there's a (less known) trick to avoid the newtype wrapper, use full qualified path of Vec (alias would also work I think):

#[derive(Parser, Debug)]
pub struct Args {
    #[arg(
        long,
        value_name = "MY_STRUCTS",
        help = "an array of MyStructs",
    )]
-    my_structs: Vec<MyStruct>
+    my_structs: ::std::vec::Vec<MyStruct>
}

see #4626

2 Likes

You nailed it, I was definitely misunderstanding how Vec is used by clap!

I'll go easy on myself since the different type behaviors are funky. E.g., I have another field in my Args that is Vec<String>, that does parse into a vec from a single value as long as I add this above it:

#[arg( value_delimiter = ',' )]

I definitely have a lot to learn about the nuances of this crate!


I like the thin wrapper idea a lot, and it would actually work just fine in my specific app. I'll consider doing that.

1 Like

OK, that's just wild! This totally works. I just had to add one more bit of code for value_parser to get it all working:

Args {
    #[arg(
        long,
        help = "JSON array of MyStructs.",
        value_name = "MY_STRUCTS",
        value_parser(parse_my_struct_array)
    )]
    error_rules: ::std::vec::Vec<MyStruct>,
}

fn parse_my_struct_array(val: &str) -> Result<Vec<MyStruct>, String> {
    let rules = serde_json::from_str(val).map_err(|e| e.to_string())?;
    Ok(rules)
}

Thank you!

1 Like

Wow, that's a super neat trick! But to be honest, I'm a little horrified it works :laughing:
I'll definitely have to file that away in my head somewhere! Thanks

1 Like

Me too. I'm tempted to mark it as "Solution", but it also feels like it might just break someday, whereas a newtype would be more future-proof.