While working on an API that was congenital specifically for mobile clients, I ran into an interesting problem that I couldn't believe I hadn't found before. When working on a REST API that deals exclusively in JSON payloads, how do you upload images? Which naturally leads onto the next question, should a JSON API then brainstorm accepting multipart form data? Is that non going to look weird that for every endpoint, we take JSON payloads, but and then for this one we take a multipart form? Because we will desire to exist uploading metadata with the image, we are going to have to read out formdata values also. That seems so 2000'south! And so allow's encounter what we can do.

While some of the examples inside this mail service are going to be using .NET Cadre, I think this really applies to any language that is being used to build a RESTful API. Then even if you aren't a C# guru, read on!

Initial Thoughts

My initial thoughts sort of boiled down to a couple of points.

  • The API I was working on had been hardcoded in a couple of areas to actually exist forcing the whole JSON payload matter. Adding in the ability to accept formdata/multipart forms would exist a little scrap of work (and regression testing).
  • We had custom JSON serializers for things like decimal rounding that would somehow manually need to exist washed for form data endpoints if required. We are even using snake_case every bit property names in the API (dang iOS developers!), which would have to exist done differently in the course information mail.
  • And finally, is there any mode to just serialize what would have been sent under a multi-part grade post, and include it in a JSON payload?

What Else Is Out There?

Information technology really became clear that I didn't know what I was doing. Then like any good programmer, I looked to copy. And then I took a look at the public API'south of the three social media giants.

Twitter

API Medico : https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload

Twitter has 2 different ways to upload files. The first is sort of a "chunked" way, which I assume is because you can upload some pretty large videos these days. And a more unproblematic way for only uploading general images, let'southward focus on the latter.

It's a multi-role class, but returns JSON. Boo.

The very very interesting part about the API nonetheless, is that it allows uploading the bodily information in two means. Either you can upload the raw binary data as y'all typically would in a multipart form postal service, or you could actually serialise the file as a Base64 encoded cord, and transport that as a parameter.

Base64 encoding a file was interesting to me because theoretically (And we nosotros will see afterwards, definitely), we can send this cord data any way nosotros like. I would say that of all the C# SDKs I looked at, I couldn't detect any actually using this Base64 method, then in that location weren't any dandy examples to get off.

Some other interesting point about this API is that you are uploading "media", and then at a later on date attaching that to an bodily object (For example a tweet). Then if you wanted to tweet out an prototype, it seems like yous would (right me if I'm wrong) upload an paradigm, get the ID returned, and then create a tweet object that references that media ID. For my use case, I certainly didn't want to do a two step procedure like this.

LinkedIn

API Doc : https://developer.linkedin.com/docs/guide/v2/shares/rich-media-shares#upload

LinkedIn was interesting because it'southward a pure JSON API. All information POSTs contain JSON payloads, like to the API I was creating. Wouldn't you guess it, they use a multipart class data besides!

Like to Twitter, they also take this concept of uploading the file first, and attaching it to where y'all actually want it to end upwards second. And I totally get that, it's but not what I want to do.

Facebook

API Medico : https://developers.facebook.com/docs/graph-api/photo-uploads

Facebook uses a Graph API. Then while I wanted to take a look at how they did things, so much of their API is non really relevant in a RESTful world. They do employ multi-part forms to upload data, but information technology'due south kinda hard to say how or why that is the example,. Also at this point, I couldn't get my mind off how Twitter did things!

So Where Does That Leave The states?

Well, in a weird mode I recall I got what I expected, That multipart forms were well and truly live. Information technology didn't seem like there was any great innovation in this area. In some cases, the use of multipart forms didn't wait so savage considering they didn't need to upload metadata at the same time. Therefore but sending a file with no fastened data didn't look so out of place in a JSON API. However, I did desire to send metadata in the same payload equally the image, not have it as a two step process.

Twitter'southward apply of Base64 encoding intrigued me. It seemed like a pretty good option for sending data beyond the wire irrespective of how yous were formatting the payload. Y'all could ship a Base64 string as JSON, XML or Form Data and it would all exist handled the same. It'southward definitely proof of concept time!

Base64 JSON API POC

What nosotros want to practise is just examination that we tin upload images equally a Base64 string, and we don't have whatever major problems within a super simple scenario. Note that these examples are in C# .Net Core, but over again, if you lot are using any other language information technology should be fairly simple to translate these.

Start, we need our upload JSON Model. In C# it would exist :

public class UploadCustomerImageModel { 	public string Clarification { get; set up; } 	public cord ImageData { get; set; } }

Non a whole lot to it. Just a description field that can be freetext for a user to draw the paradigm they are upload, and an imagedata field that will hold our Base64 string.

For our controller :

[HttpPost("{customerId}/images")] public FileContentResult UploadCustomerImage(int customerId, [FromBody] UploadCustomerImageModel model) { 	//Depending on if yous want the byte array or a memory stream, you can use the below.  	var imageDataByteArray = Convert.FromBase64String(model.ImageData); 	//When creating a stream, you need to reset the position, without it y'all volition see that you e'er write files with a 0 byte length.  	var imageDataStream = new MemoryStream(imageDataByteArray); 	imageDataStream.Position = 0; 	//Go and practise something with the bodily information. 	//_customerImageService.Upload([...]) 	//For the purpose of the demo, nosotros return a file so we can ensure information technology was uploaded correctly.  	//Just otherwise yous can only return a 204 etc.  	return File(imageDataByteArray, "image/png"); }

Again, fairly damn uncomplicated. We accept in the model, and so C# has a great way to catechumen that string into a byte array, or to read it into a memory stream. Also note that as we are merely building a proof of concept, I echo out the image data to brand certain that it's been received, read, and output like I expect information technology would, but not a whole lot else.

At present let's open upwardly postman, our JSON payload is going to await a bit similar :

{ 	"clarification" : "Test upload of an epitome",  	"imageData" : "/9j[...]" }

I've obviously truncated imagedata down here, just a super simple tool to turn an image into a Base64 is something similar this website here. I would also notation that when you send your payload, information technology should be without the data:image/jpeg;base64, prefix that you sometimes see with online tools that convert images to strings.

Hit send in Postman and :

Great! And so my image upload worked and the picture of my cat was repeat'd back to me! At this point I was actually kinda surprised that it could be that piece of cake.

Something that became very axiomatic while doing this though, was that the payload size was much larger than the original paradigm. In my instance, the prototype itself is 109KB, but the Base64 version was 149KB. And so about 136% of the original image. In having a quick search around, it seems expected that a Base64 version of a file would be almost 33% bigger than the original. When information technology comes to larger files, I call back less virtually sending 33% more beyond the wire, merely more the fact of reading the file into retention, then converting it into a huge string, and then writing that out… It could cause a few bug. Only for a few basic images, I'm comfortable with a 33% increase.

I will also notation that there was a few code snippets around for using BSON or Protobuf to do the aforementioned thing, and may really cut downwards the payload size substantially. The mentality would be the same, a JSON payload with a "stringify'd" file.

Cleaning Up Our Code Using JSON Converters

One thing that I didn't like in our POC was that we are using a string that almost certainly will be converted to a byte array every single fourth dimension. The great thing well-nigh using a JSON library such every bit JSON.net in C#, is that how the customer sees the model and how our backend code sees the model doesn't necessarily have to be the exact same. So let's encounter if we can plough that string into a byte array on an API POST automagically.

First we need to create a "Custom JSON Converter" course. That code looks like :

public class Base64FileJsonConverter : JsonConverter { 	public override bool CanConvert(Blazon objectType) 	{ 		return objectType == typeof(string); 	} 	 	public override object ReadJson(JsonReader reader, Blazon objectType, object existingValue, JsonSerializer serializer) 	{ 		return Convert.FromBase64String(reader.Value as string); 	} 	//Because we are never writing out equally Base64, nosotros don't need this.  	public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 	{ 		throw new NotImplementedException(); 	} }

Adequately unproblematic, all we are doing is taking a value and converting it from a string into a byte assortment. Besides note that we are only worried about reading JSON payloads here, we don't intendance about writing as we never write out our image every bit Base64 (yet).

Next, we had back to our model and we apply the custom JSON Converter.

public class UploadCustomerImageModel {     public string Description { go; fix; }     [JsonConverter(typeof(Base64FileJsonConverter))]     public byte[] ImageData { get; set; } }

Note we besides change the "type" of our ImageData field to a byte assortment rather than a string. So even though our postman test will still ship a cord, by the time it actually gets to us, it volition be a byte array.

We volition too need to modify our Controller code too :

[HttpPost("{customerId}/images")] public FileContentResult UploadCustomerImage(int customerId, [FromBody] UploadCustomerImageModel model) {     //Depending on if you want the byte array or a retention stream, you tin utilise the beneath.      //THIS IS NO LONGER NEEDED AS OUR MODEL NOW HAS A BYTE ARRAY     //var imageDataByteArray = Convert.FromBase64String(model.ImageData);     //When creating a stream, you need to reset the position, without information technology you will see that you e'er write files with a 0 byte length.      var imageDataStream = new MemoryStream(model.ImageData);     imageDataStream.Position = 0;     //Get and practise something with the bodily information.     //_customerImageService.Upload([...])     //For the purpose of the demo, nosotros render a file so nosotros can ensure it was uploaded correctly.      //But otherwise you can simply render a 204 etc.      return File(model.ImageData, "image/png"); }

So it becomes even simpler. We no longer demand to carp handling the Base64 encoded string anymore, the JSON converter will handle it for the states.

And that'due south information technology! Sending the exact same payload will even so work and we have one less slice of plumbing to do if nosotros determine to add more than endpoints to accept file uploads. Now you are probably thinking "Yes simply if I add in a new endpoint with a model, I still need to think to add the JsonConverter attribute", which is truthful. But at the same time, it means if in the future you decide to swap to BSON instead of Base64, y'all aren't going to have to go to a tonne of places and work out how yous are treatment the incoming strings, it's all in ane handy identify.