Can I unify those 4 function copies into one?

Can I somehow unify those 4 function copies into one?

fn json_get_str(obj: &JsonValue, key: &str) -> Result<String, String> {
    let Some(val) = obj.get(key) else {
        return Err(format!("json: didn't contain {key}!"));
    };
    match val.as_str() {
        Some(str) => Ok(str.to_owned()),
        None => Err(format!("json: {key} did not contain a String!")),
    }
}

fn json_get_i64(obj: &JsonValue, key: &str) -> Result<i64, String> {
    let Some(val) = obj.get(key) else {
        return Err(format!("json: didn't contain {key}!"));
    };
    match val.as_i64() {
        Some(num) => Ok(num),
        None => Err(format!("json: {key} did not contain a Number!")),
    }
}

fn json_get_f64(obj: &JsonValue, key: &str) -> Result<f64, String> {
    let Some(val) = obj.get(key) else {
        return Err(format!("json: didn't contain {key}!"));
    };
    match val.as_f64() {
        Some(num) => Ok(num),
        None => Err(format!("json: {key} did not contain a Number!")),
    }
}

fn json_get_bool(obj: &JsonValue, key: &str) -> Result<bool, String> {
    let Some(val) = obj.get(key) else {
        return Err(format!("json: didn't contain {key}!"));
    };
    match val.as_bool() {
        Some(bit) => Ok(bit),
        None => Err(format!("json: {key} did not contain a boolean!")),
    }
}

Also it would be nice if the String version could return a reference linked to the lifetime of the JsonValue reference we receive instead of a copy - is there a way to do that?

We're using the serde_json crate for the json part.

I am not sure if there is a better way but here is my attempt.

use serde::de::DeserializeOwned;
use serde_json::Value as JsonValue; // 1.0.140

fn json_get<T>(obj: &JsonValue, key: &str) -> Result<T, String>
where
    T: DeserializeOwned,
{
    let Some(val) = obj.get(key) else {
        return Err(format!("json: didn't contain {key}!"));
    };

    serde_json::from_value(val.clone())
        .map_err(|_| format!("json: {key} did not contain a String!"))
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let data = r#"
        {
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        }"#;
    let v: JsonValue = serde_json::from_str(data)?;

    assert_eq!(json_get::<i64>(&v, "age")?, 43);
    assert_eq!(json_get::<String>(&v, "name")?, "John Doe");
    Ok(())
}
1 Like

You could create a trait with a method and implement it for each type, to move the as_i64() and friends code out of the function and into the trait implementations.

Then you take chung's approach but bound the generic parameter on your trait instead of deserialize owned.

It'll still feel like a lot of duplication to you but presents a single (generic) method to your users.

Your trait might need an associated type for the output, so that you can get &str and return String, although you might be able to use lifetimes to get the &str linked to the &JsonValue.

I've given this a go (in particular returning a borrow for one implementation) and am unsure how to make the lifetimes work. I get a mismatch between the lifetime of &JsonValue in my trait definition and the lifetime of &'a str in the Self parameter of one of the implementations. Which is fair!

It feels like I might need a borrowing version of the trait (but then there'd be two functions), or possibly a GAT and an Output associated type. But it's unclear to me if I can return something from a trait method that might borrow from the input or might be owned based on the type the trait is implemented for (Self) - I have a feeling by returning <Self as Trait>::Output I can do this, hence the need for GAT to give that a lifetime.

(without explicitly returning an enum like Cow).

Here's a way with the generic on the method, so that it's turbofishable at the call site.

2 Likes