Question

Converting Between String, &str, Vec<u8>, and &[u8] in Rust

ruststringtype-conversion

Question

I am new to Rust and find it difficult to work comfortably with these related types: String, &str, Vec<u8>, and &[u8].

I want to understand the idiomatic ways to convert between them and, more importantly, when each conversion is appropriate.

Given these values:

let st: &str = "hello";
let s: String = String::from("hello");
let u: &[u8] = b"hello";
let v: Vec<u8> = b"hello".to_vec();

I believe the following conversions are correct, but I am not sure whether they are the most idiomatic choices:

// &str -> String
String::from(st)

// &str -> &[u8]
st.as_bytes()

// String -> &str
s.as_str()

// &[u8] -> &str
std::str::from_utf8(u)

// Vec<u8> -> String
String::from_utf8(v)

I would like a complete table of idiomatic transitions for all of these cases:

  • &str -> String
  • &str -> &[u8]
  • &str -> Vec<u8>
  • String -> &str
  • String -> &[u8]
  • String -> Vec<u8>
  • &[u8] -> &str
  • &[u8] -> String
  • &[u8] -> Vec<u8>
  • Vec<u8> -> &str
  • Vec<u8> -> String
  • Vec<u8> -> &[u8]

Short Answer

By the end of this page, you will understand how Rust converts between text types and byte types, when a conversion is free versus when it allocates, and which conversions can fail because of UTF-8 validation. You will also see the idiomatic methods for each direction and learn how ownership and borrowing affect your choices.

Concept

Rust separates text from raw bytes, and it also separates owned data from borrowed data.

These four types sit at the intersection of those two ideas:

TypeMeaningOwns data?Must be valid UTF-8?
&strBorrowed string sliceNoYes
StringOwned growable stringYesYes
&[u8]Borrowed byte sliceNoNo
Vec<u8>Owned growable byte bufferYesNo

The key idea is:

  • str and String represent text
  • [u8] and Vec<u8> represent bytes
  • Borrowed types (&str, &[u8]) do not own memory
  • Owned types (String, Vec<u8>) own memory and may allocate

Why this matters

In real Rust code:

  • APIs that process text often take &str
  • APIs that store or build text often use String
  • File, network, and binary APIs often use &[u8] or Vec<u8>
  • Converting bytes to text may fail if the bytes are not valid UTF-8

This is why some conversions are simple borrows, some require cloning or allocation, and some return Result.

A useful rule

Ask two questions:

  1. Am I switching between borrowed and owned data?
    • If yes, you may need to clone or move ownership.
  2. Am I switching between bytes and text?
    • If yes, UTF-8 validity matters.

Complete conversion table

FromToIdiomatic conversionAllocates?Can fail?
&strStringst.to_string() or String::from(st)YesNo
&str&[u8]st.as_bytes()NoNo
&strVec<u8>st.as_bytes().to_vec()YesNo
String&strs.as_str() or &sNoNo
String&[u8]s.as_bytes()NoNo
StringVec<u8>s.into_bytes()No extra copyNo
&[u8]&strstd::str::from_utf8(u)NoYes
&[u8]StringString::from_utf8(u.to_vec())YesYes
&[u8]Vec<u8>u.to_vec()YesNo
Vec<u8>&strstd::str::from_utf8(&v)NoYes
Vec<u8>StringString::from_utf8(v)No extra copyYes
Vec<u8>&[u8]&v or v.as_slice()NoNo

Important UTF-8 note

Rust strings are always valid UTF-8. That means:

  • &str -> &[u8] is always safe
  • String -> Vec<u8> is always safe
  • &[u8] -> &str may fail
  • Vec<u8> -> String may fail

That single UTF-8 rule explains most of the API design around these types.

Mental Model

Think of these types as two axes:

  • Text vs bytes
  • Borrowed vs owned

Imagine a 2×2 grid:

BorrowedOwned
Text&strString
Bytes&[u8]Vec<u8>

Now think of conversions as moves on this grid:

  • Moving left/right changes ownership
  • Moving up/down changes representation

Horizontal moves

  • &str -> String: make an owned copy
  • String -> &str: borrow from owned text
  • &[u8] -> Vec<u8>: copy bytes into owned storage
  • Vec<u8> -> &[u8]: borrow bytes from the vector

Vertical moves

  • &str -> &[u8]: text viewed as UTF-8 bytes
  • String -> Vec<u8>: owned text turned into owned bytes
  • &[u8] -> &str: bytes interpreted as text, only if valid UTF-8
  • Vec<u8> -> String: owned bytes interpreted as text, only if valid UTF-8

Simple analogy

  • String is like a book you own
  • &str is like reading a page from that book
  • Vec<u8> is like a box of raw paper scraps you own
  • &[u8] is like looking at some scraps without owning them

You can always look at text as bytes, because text is stored as bytes. But you cannot always treat bytes as text, because not every byte sequence is valid UTF-8 text.

Take Quiz

Syntax and Examples

Core syntax

let st: &str = "hello";
let s: String = String::from("hello");
let u: &[u8] = b"hello";
let v: Vec<u8> = b"hello".to_vec();

Borrowed text to owned text

let owned: String = st.to_string();
let owned2: String = String::from(st);

Both are idiomatic. to_string() is often the most readable for beginners.

Borrowed text to borrowed bytes

let bytes: &[u8] = st.as_bytes();

No allocation happens here. You are just viewing the string as bytes.

Borrowed text to owned bytes

let bytes_vec: Vec<u8> = st.as_bytes().to_vec();

This creates a new vector containing a copy of the bytes.

Owned text to borrowed text

let text: &str = s.as_str();
// or simply:
let text2: &str = &s;

Both are idiomatic.

Owned text to borrowed bytes

let bytes: &[u8] = s.as_bytes();

No copy. You borrow the bytes inside the String.

Owned text to owned bytes

let s = String::from("hello");
let bytes: Vec<u8> = s.into_bytes();

into_bytes() consumes the String and reuses its buffer.

Borrowed bytes to borrowed text

let u: &[u8] = b"hello";
let text: Result<&str, std::str::Utf8Error> = std::str::from_utf8(u);

This returns Result because bytes may not be valid UTF-8.

Borrowed bytes to owned text

let u: &[u8] = b"hello";
let text: Result<String, std::string::FromUtf8Error> = String::from_utf8(u.to_vec());

You first create an owned Vec<u8>, then try to convert it into String.

A common alternative is:

let text: Result<String, std::str::Utf8Error> =
    std::str::from_utf8(u).map(str::to_owned);

Borrowed bytes to owned bytes

let copy: Vec<u8> = u.to_vec();

Owned bytes to borrowed text

let v: Vec<u8> = b"hello".to_vec();
let text: Result<&str, std::str::Utf8Error> = std::str::from_utf8(&v);

Owned bytes to owned text

let v: Vec<u8> = b"hello".to_vec();
let text: Result<String, std::string::FromUtf8Error> = String::from_utf8(v);

This consumes the vector.

Owned bytes to borrowed bytes

let slice: &[u8] = &v;
// or
let slice2: &[u8] = v.as_slice();

Step by Step Execution

Consider this example:

fn main() {
    let s = String::from("hi");
    let bytes = s.as_bytes();
    let copied = bytes.to_vec();
    let text = std::str::from_utf8(&copied).unwrap();

    println!("{}", text);
}

Step by step

1. Create an owned string

let s = String::from("hi");
  • s owns the text "hi"
  • Internally, that text is stored as UTF-8 bytes

2. Borrow its bytes

let bytes = s.as_bytes();
  • bytes is a &[u8]
  • No new memory is allocated
  • bytes points into the same data owned by s

At this point, the bytes are roughly:

[104, 105]

because h = 104 and i = 105

3. Copy the bytes into a new vector

let copied = bytes.to_vec();
  • copied is a new Vec<u8>
  • This allocates memory and copies the bytes
  • Now copied owns its own byte buffer

4. Try to interpret the bytes as UTF-8 text

let text = std::str::from_utf8(&copied).unwrap();
  • &copied becomes a &[u8]
  • from_utf8 checks whether those bytes are valid UTF-8
  • Since [104, 105] is valid UTF-8, it returns Ok("hi")
  • unwrap() extracts the &str

5. Print the result

println!("{}", text);

Output:

hi

What to notice

  • as_bytes() borrows
  • to_vec() copies
  • from_utf8() validates
  • Borrowed forms are cheap
  • Owned forms may allocate or move data

Real World Use Cases

Working with files and network data

File and socket APIs commonly give you bytes:

fn handle_packet(data: &[u8]) {
    if let Ok(text) = std::str::from_utf8(data) {
        println!("Text message: {}", text);
    } else {
        println!("Binary message: {:?}", data);
    }
}

Use &[u8] when the data may be binary or when you want to avoid assuming UTF-8.

Building HTTP responses

Web frameworks often accept text as String or &str, but lower-level protocols may need bytes:

let body = String::from("OK");
let raw: Vec<u8> = body.into_bytes();

Parsing user input

CLI tools often read text and borrow it as &str for parsing:

fn is_yes(input: &str) -> bool {
    input.trim() == "yes"
}

&str is ideal when you only need to read text.

Storing generated content

If your program builds text dynamically, use String:

let mut report = String::new();
report.push_str("Status: ");
report.push_str("ready");

Processing binary formats

Image formats, compressed data, protobuf messages, and encrypted payloads are usually handled as Vec<u8> or &[u8], not strings.

Converting API payloads

JSON and text-based protocols often arrive as bytes and must be validated as UTF-8 before parsing:

fn parse_json_bytes(data: &[u8]) {
    let text = std::str::from_utf8(data).expect("expected UTF-8 JSON");
    println!("JSON text: {}", text);
}
Take Quiz

Real Codebase Usage

In real Rust codebases, developers usually choose the most flexible input type and the most useful ownership model.

Accept borrowed data in function parameters

Functions often accept &str or &[u8] instead of owned types:

fn log_message(msg: &str) {
    println!("{msg}");
}

fn write_bytes(buf: &[u8]) {
    println!("{} bytes", buf.len());
}

This lets callers pass either borrowed or owned values.

Use &str for read-only text APIs

fn greet(name: &str) -> String {
    format!("Hello, {name}!")
}

Callers can pass:

  • string literals
  • &String
  • slices of strings

Use String when ownership is needed

Store text in structs when it must outlive the current scope:

struct User {
    name: String,
}

Guard clauses for UTF-8 validation

fn parse_text(data: &[u8]) -> Result<&str, std::str::Utf8Error> {
    let text = std::str::from_utf8(data)?;
    Ok(text)
}

This is a common pattern: validate early, then work with &str.

Avoid unnecessary allocation

A common pattern is to borrow first and only allocate if needed:

fn print_uppercase(data: &[u8]) {
    if let Ok(text) = std::str::from_utf8(data) {
        println!("{}", text.to_uppercase());
    }
}

The borrowed &str avoids making a String until to_uppercase() creates one.

Convert owned text into owned bytes efficiently

fn send_payload(text: String) -> Vec<u8> {
    text.into_bytes()
}

into_bytes() is preferred when you already own the String.

Common pattern: AsRef<[u8]> and AsRef<str>

Many libraries accept generic borrowed input:

fn checksum<T: AsRef<[u8]>>(data: T) -> usize {
    data.as_ref().len()
}

This allows callers to pass &[u8], Vec<u8>, String, and &str in some contexts.

Common Mistakes

1. Assuming all byte slices are valid text

Broken code:

let bytes = &[0xff, 0xfe, 0xfd];
let text = std::str::from_utf8(bytes).unwrap();

This may panic because the bytes are not valid UTF-8.

Better:

match std::str::from_utf8(bytes) {
    Ok(text) => println!("{text}"),
    Err(err) => println!("Invalid UTF-8: {err}"),
}

2. Cloning when borrowing would be enough

Broken idea:

fn print_name(name: String) {
    println!("{name}");
}

This forces ownership unnecessarily.

Better:

fn print_name(name: &str) {
    println!("{name}");
}

3. Using to_vec() when into_bytes() is better

Less efficient:

let s = String::from("hello");
let v = s.as_bytes().to_vec();

This copies bytes.

Better when you own s:

let s = String::from("hello");
let v = s.into_bytes();

4. Trying to return &str from temporary data

Broken code:

fn make_text() -> &str {
    let s = String::from("hello");
    s.as_str()
}

This does not compile because s is dropped at the end of the function.

Better:

fn make_text() -> String {
    String::from("hello")
}

5. Forgetting that String::from_utf8 consumes the vector

let v = b"hello".to_vec();
let s = String::from_utf8(v);

After this, v has been moved and cannot be used again unless you clone it beforehand.

6. Confusing &String with &str

&String works in some places, but APIs usually prefer &str because it is more general.

Better function signature:

fn process(text: &str) {
    println!("{text}");
}

7. Ignoring lossy conversions when appropriate

Sometimes invalid UTF-8 should not cause failure.

let bytes = b"hello\xFF";
let text = String::from_utf8_lossy(bytes);
println!("{text}");

This replaces invalid bytes with the Unicode replacement character.

Comparisons

Borrowed vs owned

TypeBorrowed or owned?Mutable size?Typical use
&strBorrowedNoRead-only text input
StringOwnedYesStore or build text
&[u8]BorrowedNoRead-only bytes
Vec<u8>OwnedYesStore or build byte data

Text vs bytes

TypeRepresents text?Represents arbitrary bytes?UTF-8 guaranteed?
&strYesNoYes
StringYesNoYes
&[u8]SometimesYesNo
Vec<u8>SometimesYesNo

Similar conversions compared

ConversionBest choiceWhy
&str -> Stringto_string() or String::from(...)Clear and idiomatic
String -> &str&s or s.as_str()Borrow without allocation
String -> Vec<u8>into_bytes()Reuses owned buffer
Vec<u8> -> StringString::from_utf8(v)Validates UTF-8 and reuses buffer
&[u8] -> &strstd::str::from_utf8(u)Borrow plus validation
&[u8] -> Stringstd::str::from_utf8(u)?.to_owned()Clear when starting from borrowed bytes

from_utf8 vs from_utf8_lossy

MethodReturn typeFails on invalid UTF-8?Use when
std::str::from_utf8Result<&str, Utf8Error>YesYou need valid UTF-8
String::from_utf8Result<String, FromUtf8Error>YesYou own the bytes
String::from_utf8_lossyCow<str>NoYou want readable text even if bytes are invalid
Take Quiz

Cheat Sheet

Quick reference

// Starting values
let st: &str = "hello";
let s: String = String::from("hello");
let u: &[u8] = b"hello";
let v: Vec<u8> = b"hello".to_vec();

Conversion table

// &str -> String
let a: String = st.to_string();
let a2: String = String::from(st);

// &str -> &[u8]
let b: &[u8] = st.as_bytes();

// &str -> Vec<u8>
let c: Vec<u8> = st.as_bytes().to_vec();

// String -> &str
let d: &str = s.as_str();
let d2: &str = &s;

// String -> &[u8]
let e: &[u8] = s.as_bytes();

// String -> Vec<u8>
let f: Vec<u8> = s.into_bytes();

// &[u8] -> &str
let g = std::str::from_utf8(u); // Result<&str, Utf8Error>

// &[u8] -> String
let h = std::str::from_utf8(u).map(str::to_owned);
// or
let h2 = String::from_utf8(u.to_vec());

// &[u8] -> Vec<u8>
let i: Vec<u8> = u.to_vec();

// Vec<u8> -> &str
let j = std::str::from_utf8(&v); // Result<&str, Utf8Error>

// Vec<u8> -> String
let k = String::from_utf8(v); // Result<String, FromUtf8Error>

// Vec<u8> -> &[u8]
let l: &[u8] = &v;
let l2: &[u8] = v.as_slice();

Rules to remember

  • String and &str are always valid UTF-8
  • Vec<u8> and &[u8] may contain any bytes
  • Borrowing is cheap
  • Cloning or to_vec() allocates
  • into_* methods usually consume and reuse owned data
  • Byte-to-text conversions often return Result

Best default choices

  • Need read-only text input: use &str
  • Need owned text: use String
  • Need read-only bytes: use &[u8]
  • Need owned bytes: use Vec<u8>

FAQ

When should I use &str instead of String in Rust?

Use &str when your function only needs to read text and does not need ownership. It is more flexible and avoids unnecessary allocation.

Why does &[u8] to &str return a Result?

Because not all byte slices contain valid UTF-8. Rust strings must always be valid UTF-8, so validation is required.

What is the most idiomatic way to convert String to Vec<u8>?

Use into_bytes() if you own the String. It consumes the string and reuses its buffer efficiently.

How do I convert Vec<u8> to String in Rust?

Use String::from_utf8(v). It checks UTF-8 validity and returns Result<String, FromUtf8Error>.

Is &String the same as &str?

Not exactly. &String is a reference to a String, while &str is a string slice. Many APIs prefer &str because it works with more kinds of string data.

What if my bytes are not valid UTF-8 but I still want a string?

Use String::from_utf8_lossy(bytes). Invalid byte sequences are replaced with the replacement character.

Does as_bytes() copy the data?

No. It returns a borrowed byte slice referencing the existing string data.

How do I choose between to_vec() and into_bytes()?

Use to_vec() when you only have a borrow and need an owned byte vector. Use into_bytes() when you already own the String and want to avoid copying.

Take Quiz

Mini Project

Description

Build a small Rust utility that accepts incoming byte data, tries to interpret it as UTF-8 text, and then prints both the text form and the raw bytes form. This demonstrates the most common conversions between borrowed and owned text/byte types in a realistic workflow similar to reading data from a file, socket, or API response.

Goal

Create a program that starts with byte data, validates it as UTF-8, converts it into both borrowed and owned text, and also shows how to convert text back into bytes.

Requirements

  • Start with a Vec<u8> containing UTF-8 text bytes.
  • Borrow the bytes as &[u8] and attempt to convert them to &str.
  • Create an owned String from the bytes.
  • Convert the String back into Vec<u8>.
  • Print each step so the data flow is visible.
Take Quiz

Keep learning

Related questions

View more