Yes. Sending JSON is one of the most routine jobs developers hand to an HTTP client, and OkHttp handles it with the same building blocks you already use for GET requests. A JSON post is simply an HTTP POST whose payload is a JSON document instead of form fields. You wrap the JSON text in a request body, label it with the correct media type, attach it to the request, and let the client send it.
If you already use OkHttp for ordinary requests, almost nothing about your setup changes. The client stays the same, the request builder stays the same, and the way you execute a call stays the same. Only the request gains a body. That is worth keeping in mind, because most JSON posting problems are not exotic at all. They usually trace back to a wrong media type, a body built in a surprising way, or a call launched on the wrong thread.
This article walks through the process in the order you would write the code: what the request body holds, how the media type describes it, how headers behave, how to read the answer, and the few mistakes that make JSON posts fail in confusing ways.
What the Request Body Actually Holds
Every POST request carries a body, which is the block of bytes that travels after the headers. For a JSON post, those bytes are the UTF-8 encoding of your JSON document: the braces, keys, strings and numbers, exactly as text. OkHttp does not inspect or interpret this content. As far as the client is concerned, it is moving bytes from your app to the server as reliably as it can.
This separation is liberating once you see it. OkHttp is responsible for transport: opening the connection, writing the bytes, and reading the reply. Your code is responsible for meaning: deciding what the JSON should contain and producing valid JSON text. When a post fails, the first diagnostic question is which of those two jobs went wrong, because the fixes are completely different.
The Media Type Is the Label the Server Reads
Alongside the body, every request body carries a media type, and for JSON that label is application/json. The server reads this value from the Content-Type header to decide how to parse what arrived. If the label says JSON, the server hands the bytes to a JSON parser. If the label says something else, the server may try a different parser or reject the request outright.
This is why a perfectly valid JSON document can still be refused. The document is fine; the label is wrong. A common variant is sending JSON while the body was created with a form media type, so the server tries to read your braces and quotes as form fields and gives up. When you build the body, the media type and the content must agree, and for JSON posts the media type to use is application/json, optionally with a charset note.
Building the Body From a Plain String
The simplest way to create a JSON body is from a string you already have. You convert the string into a request body together with the JSON media type, then set that body on the request builder with the post method. The whole exchange looks like this:
val json = """{"title":"Morning run","distance":12.5}"""
val body = json.toRequestBody("application/json; charset=utf-8".toMediaType())
val request = Request.Builder()
.url("https://example.org/activities")
.post(body)
.build()
client.newCall(request).execute().use { response ->
println(response.code)
}
In practice the JSON string rarely appears literally in code. It is usually produced by serializing an object: a data class or model that your code fills in, converted to JSON text just before the request is built. That conversion step belongs to your app, and any serializer you already use for reading responses can normally do it. OkHttp only receives the finished text.
PUT and PATCH Use the Same Machinery
JSON is not tied to POST. Endpoints that update a resource often expect PUT or PATCH, and both take a request body in exactly the same way. You build the body once, then choose the method on the request builder that matches the verb: put for a full replacement document, patch for a partial update. The media type stays application/json, and the response handling stays identical.
The practical difference between these verbs lives on the server, not in your client code. What matters for you is consistency: the verb, the URL and the document shape form a contract with the API. If a post fails with a method-related error, switching verbs in OkHttp is a one-word change, because the body construction underneath never changes.
Form-Encoded Bodies Are the Main Alternative
Some endpoints still expect classic form data: field names and values encoded as key=value pairs rather than as a JSON document. OkHttp covers that case with a form body builder, where you add each field by name and let the client encode the result. The resulting request looks similar on the surface, but the media type is different and the server parses it differently, so the two styles are not interchangeable.
Files are a third case again. When an upload includes an actual file, the payload becomes a multipart body that mixes form fields with binary parts, which is a bigger topic covered in our guide to upload and download files with OkHttp. For plain data submission, though, the choice is simply between JSON and form encoding, and the API documentation decides which one the server wants.
The Headers That Travel With the Body
Two headers deserve attention on every JSON post. The first is Content-Type, which OkHttp sets from the media type you gave the body, so you should not set it again by hand. The second is Content-Length, which the client calculates itself once it knows the body’s size. For a body built from a string, that size is known immediately, so the request goes out with an exact length.
Beyond those, the headers you add are the usual ones: an Authorization header when the endpoint is protected, and an Accept header when you want to state that you expect JSON back. Neither is specific to posting. What is specific is avoiding duplication: if you set Content-Type manually and also give the body a media type, you can end up with conflicting signals. Let the body speak for itself, and add only the headers that are genuinely yours to add.
Reading the Response and Checking the Status
Sending is only half of the exchange. When the call completes, the response carries a status code and usually a body of its own, often a JSON document describing the created resource or an error. Read the code first. The isSuccessful check divides the world neatly: codes in the 200 range mean the server accepted your document, while 400 signals a problem with what you sent and 401 or 403 point at credentials or permissions.
Whatever the outcome, close the response. Wrapping the call in a use block, or a try-with-resources statement in Java, guarantees the body is consumed and its connection returns to the pool for reuse. A response left open pins its connection, and enough pinned connections will stall later requests. Treat reading and closing as part of the post itself, not as cleanup you might get around to afterwards.
Posts Belong on a Background Thread
OkHttp offers two ways to run a call: execute for synchronous calls and enqueue for asynchronous ones. A synchronous post blocks the thread it runs on until the server answers, which may take seconds on a poor connection. On Android, running network work on the main thread is not merely slow, it is forbidden, and the platform throws an exception as soon as the call attempts real network I/O.
So the rule is simple: post from a background thread, a worker, or through the asynchronous enqueue API with a callback. The callback itself also runs on a background thread, so if the result needs to reach the screen, hand it over to the main thread deliberately at the end. Keeping the post off the UI thread protects both the request and the responsiveness of the app around it.
The Release-Build Trap: Empty JSON Bodies
One failure mode surprises almost everyone the first time. The app works perfectly in debug builds, but in the release build the server keeps receiving an empty JSON object, just braces with nothing inside. The request succeeds, the media type is right, and yet the document is empty. The cause is almost never in OkHttp. It sits in the build itself.
Release builds usually shrink and obfuscate code, and the shrinking step removes class fields it believes are unused. If your model classes are only touched through reflection by your JSON serializer, the shrinker may strip or rename exactly the fields the serializer needs, so serialization quietly produces an empty document. The fix is to protect those model classes with keep rules in your shrinking configuration, or to annotate the fields so the serializer’s contract is visible to the build. Once the models survive the build intact, the same OkHttp code posts full documents again.
Charset Details That Rarely Matter, Until They Do
JSON text is encoded as characters before it becomes bytes, and the shared assumption on the modern web is UTF-8. When you create a body from a string with the application/json media type, the text is encoded as UTF-8, which is what servers expect. For everyday data, names, titles and descriptions in any language pass through without incident.
Charset trouble appears at the edges: a server that decodes with a legacy encoding, or a document assembled by hand from bytes that were read with the wrong charset earlier in the pipeline. The symptom is mangled text rather than outright failure, which makes it easy to misdiagnose as a server bug. If characters arrive garbled while the structure is intact, suspect encoding, and check that every step between your data and the request body treats the text as UTF-8.
Frequently Asked Questions
Does OkHttp convert my objects into JSON automatically?
No. OkHttp transports bytes; it does not serialize objects. Your app turns a model into JSON text using whatever serialization approach it already relies on, then wraps that text in a request body. This split is deliberate, and it is why the same client code works with any JSON tooling you prefer.
Why does the server answer 415 when I post JSON?
A 415 response means the server refused the media type of your payload. The usual cause is a body whose label does not say application/json, even though the content really is JSON. Check the media type you passed when creating the body, and make sure no manually added Content-Type header contradicts it.
Can I send the same JSON body with two different calls?
A body built from a string can be written more than once, so it can safely back more than one request. Not every body type has that property, since streaming bodies may be readable only once. When you reuse a body, build a fresh request for each call, because requests themselves are one-shot objects.
How can I see what my post actually sent?
Add a logging interceptor during development and inspect the request line, headers and body it prints, then compare them with what the server expected. Remove or restrict that logging before release, since request bodies often contain personal data that should never end up in logs.
Conclusion
Posting JSON with OkHttp comes down to three things done carefully: a body that contains genuine JSON text, a media type that honestly labels it as application/json, and a call executed off the main thread with its response read and closed. Get those right and the mechanism is boring in the best possible way.
When a post misbehaves, work through the same list in order: label, content, thread, response handling, and only then the exotic causes like release-build shrinking. Almost every JSON failure you will meet lives somewhere on that short checklist.
Pingback: How do you upload and download files with OkHttp? - OkHttp