How to add String extension to encode JSON values

I am a Rust developer, but recently I bought Mac mini and decided to try Swift. I have the following function in Rust

pub fn json_encode(orig: &impl AsRef<str>) -> String {
    let chars = orig.as_ref().chars();
    let mut res = String::from("");
    for c in chars {
        match c {
            '"' => res.push_str("\\\""),
            '\n' => res.push_str("\x5Cn"),
            '\r' => res.push_str("\x5cr"),
            '\t' => res.push_str("\x5ct"),
            '\\' => res.push_str("\x5c\x5c"),
            _ => res.push(c),
        }
    }
    res
}

What will be an idiomatic way to covert it in a Swift equivalent as a String extension?

You could write an extension on String like this

extension String {
    func jsonEncoded() -> String {
        var result = ""
        for c in self {
            switch c {
            case "\"":
                result.append("\\\"")
            case "\n":
                result.append("\\n")
            case "\r":
                result.append("\\r")
            case "\t":
                result.append("\\t")
            case "\\":
                result.append("\\\\")
            default:
                result.append(c)
            }
        }
        return result
    }
}

There's really no need to manually convert a String for JSON, it already knows how to do that when you use JSONEncoder.

1 Like

It works like a charm, thank you.

Yes, AI recommends the way too. However, I need to fill out a structure and then call JSONEncoder. Although Swift has a similar structure anatomy to Rust, I didn’t learned it yet. BTW I didn’t use Serde in Rust also, preferring the old fashion way - from parts.

Same, but a bit cleaner and more DRY:

    var jsonEncoded: String {
        reduce(into: "") { result, c in
            let symbol = switch c {
                case "\"": "\\\""
                case "\n": "\\n"
                case "\r": "\\r"
                case "\t": "\\t"
                case "\\": "\\\\"
                default: String(c)
            }
            result.append(symbol)
        }
    }

Hopefully Swift will make if/switch/do expressions fully capable at some point. Until then you could also try:

    var jsonEncoded: String {
        map {
            switch $0 {
                case "\"": "\\\""
                case "\n": "\\n"
                case "\r": "\\r"
                case "\t": "\\t"
                case "\\": "\\\\"
                default: String($0)
            }
        }.joined()
    }

but I wouldn't be surprised if this has worse memory / runtime characteristics compared to above.

Nice. Next step will be mimic Rust solution returning Cow which translates to returning the original string, if it doesn’t contain special characters.

BTW, I am getting an error trying your code snippets:

main.swift:34:5: error: cannot find 'map' in scope
 32 | 
 33 | var jsonEncoded: String {
 34 |     map {
    |     `- error: cannot find 'map' in scope
 35 |         switch $0 {
 36 |             case "\"": "\\\""

@tera meant this :

extension String {
    var jsonEncoded: String {
        map {
            switch $0 {
            case "\"": "\\\""
            case "\n": "\\n"
            case "\r": "\\r"
            case "\t": "\\t"
            case "\\": "\\\\"
            default: String($0)
            }
        }.joined()
    }
}

Yep, I meant you'd put that snippet into extension String { ... }

Do you want to implement this quickly or do you want it to work quickly? Equally important consideration – how long are the strings in question – it won't make sense to invest into a more complicated algorithm if strings are known to be short where the complex algorithm will not be able to show it's full potential (and will be slower).

If the former ("do it in under one minute") – just compare the newly accumulated string and the original string and only return the new string if the two are different. Slightly more optimal but still trivial to write would be maintaining a boolean variable that you'd set in case of encountering a special character, then when it comes to return either return the new string or the original string based on that variable.

If the latter ("make it work fast") – it's more complicated and longer to implement, I'd suggest to have two string indices (left and right) in addition to a boolean variable. While stepping through the string you advance the right index and check the character preceding it, if it's a special character then set the bool flag and append all characters within left ... right to the newly accumulated string, and adjust both indices to point to the current location. Otherwise (no special character case) just continue with the loop increasing the right index to point to the next location in the string. When it comes to return - check the boolean flag, if set then return the newly accumulated string otherwise return the original string.

Generally, the interest was how Swift good for server side applications? You perfectly resolved the question. Anyway, I like Swift and probably will use it for personal applications after I buy iPhone.