Identically formatted JSON string works with curl but gives error 400 with Java

The following OpenAI-API Input Request works as expected when invoked from curl but gives error 400 when invoked from a Java program. The Java source code files follow.
Kindly advise.

curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-4-vision-preview",
    "messages": [
      {
        "role": "user",
        "content": [
          {
            "type": "text",
            "text": "What’s in this image?"
          },
          {
            "type": "image_url",
            "image_url": {
              "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
            }
          }
        ]
      }
    ],
    "max_tokens": 300
  }'

Here are my 3 Java files.

/////////////////////////////////////////////////////////////////////
// First File: Prompt.java
/////////////////////////////////////////////////////////////////////
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
 * 
 * Class that represents the Prompt.
 * Inner classes: FileUrl, Content, Message
 *
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class Prompt {
	
	/**
	 * Inner Class FileUrl
	 *
	 */
	@JsonIgnoreProperties(ignoreUnknown = true)
	public static class FileUrl {
	    @JsonProperty("url")
	    public String url;
	    

	    public FileUrl(String url) {
	    	this.url = url;
	    }

	    @Override
	    public String toString() {
	        return JsonConverter.toJson(this);
	    }
	}
	
	/**
	 * Inner Class Content
	 *
	 */
	@JsonIgnoreProperties(ignoreUnknown = true)
	public static class Content {
	    @JsonProperty("type")
	    public String type;
		
	    @JsonProperty("text")
	    public String text;
	    
	    @JsonProperty("image_url")
	    public FileUrl image_url;
	    

	    /**
	     *
	     */
	    public Content(String type, String text) {
	    	this.type = type;
	    	this.text = text;
	    	this.image_url = null;
	    }
		

	    /**
	     *
	     */
	    public Content(String type, FileUrl image_url) {
	    	this.type = type;
	    	this.image_url = image_url;
	    	this.text = null;
	    }

	    /**
	     *
	     */
	    @Override
	    public String toString() {
	        return JsonConverter.toJson(this);
	    }
	}
	
	/**
	 * Inner Class Message
	 *
	 */
	@JsonIgnoreProperties(ignoreUnknown = true)
	public static class Message {
	    @JsonProperty("role")
	    public String role;

	    @JsonProperty("content")
	    public List<Content> content;

	    /**
	     *
	     */
	    public Message(String role, List<Content> content) {
	        this.role = role;
	        this.content = content;
	    }

	    /**
	     *
	     */
	    @Override
	    public String toString() {
	        return JsonConverter.toJson(this);
	    }
	}

	
	/**
	 * Outer Class
	 *
	 */
	@JsonProperty("model")
	public String model;

	@JsonProperty("messages")
	public List<Message> messages;

    @JsonProperty("max_tokens")
	public int max_tokens;

    /**
     *
     */
	public Prompt(String model, List<Message> messages, int max_tokens) {
    	this.model = model;
    	this.messages = messages;
    	this.max_tokens = max_tokens;
	}

    /**
     *
     */
    @Override
    public String toString() {
        return JsonConverter.toJson(this);
    }
}
/////////////////////////////////////////////////////////////////////
// Second File: JsonConverter.java
/////////////////////////////////////////////////////////////////////
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
 * Converts a Java object to its JSON string representation,
 * which is neatly formatted.  Performs some additional functions like
 * - removing null fields
 * - neatly formatting the JSON String representation
 */

public class JsonConverter {

    public static String toJson(Object object) {
        String jsonStr = null;

        try {
			ObjectMapper objectMapper = new ObjectMapper();
			
			if (object instanceof String) {
				jsonStr = (String) object;
			}
			else {
			    jsonStr = objectMapper.writeValueAsString(object);
			}
			
			if(jsonStr != null) {
				jsonStr = removeNullFields(jsonStr, objectMapper);
			}
			
			JsonNode jsonNode = objectMapper.readTree(jsonStr);
			objectMapper.enable(com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT); // Enable pretty printing
			
			jsonStr = objectMapper.writeValueAsString(jsonNode);
		}
        catch (Exception e) {
			e.printStackTrace();
		}

        return jsonStr;
    }

    protected static String removeNullFields(String jsonString, ObjectMapper mapper) throws IOException {

        // Parse the JSON string into a JsonNode
        JsonNode rootNode = mapper.readTree(jsonString);

        // Remove null fields from the JsonNode
        JsonNode cleanedNode = removeNullFields(rootNode);

        // Convert the updated JsonNode back to a JSON string
        return mapper.writeValueAsString(cleanedNode);
    }

    /**
     *
     */
    private static JsonNode removeNullFields(JsonNode node) {
        if (node.isObject()) {
            ObjectNode objectNode = (ObjectNode) node;
            List<String> keysToRemove = new ArrayList<>();
            Iterator<String> fieldNames = objectNode.fieldNames();
            while (fieldNames.hasNext()) {
                String fieldName = fieldNames.next();
                JsonNode fieldValue = objectNode.get(fieldName);
                if (fieldValue.isNull()) {
                    keysToRemove.add(fieldName);
                }
                else {
                    removeNullFields(fieldValue);
                }
            }
            keysToRemove.forEach(objectNode::remove);
        }
        else if (node.isArray()) {
            ArrayNode arrayNode = (ArrayNode) node;
            List<Integer> indicesToRemove = new ArrayList<>();
            for (int i = 0; i < arrayNode.size(); i++) {
                JsonNode element = arrayNode.get(i);
                if (element.isNull()) {
                    indicesToRemove.add(i);
                }
                else {
                    removeNullFields(element);
                }
            }
            indicesToRemove.forEach(arrayNode::remove);
        }
        return node;
    }
}
/////////////////////////////////////////////////////////////////////
// Third File: ImageInputClient.java
/////////////////////////////////////////////////////////////////////
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * Client Application
 */
public class ImageInputClient {

	public static void main(String[] args) {
		boolean debug = true;
		
    	String openAIKey = "my-openai-key-blah-blah-blah";
    	String endpoint = "https://api.openai.com/v1/chat/completions";
        String model = "gpt-4-vision-preview";
        
    	int maxTokens = 300;        
        
        // Start building the request
    	Prompt.Content contentObj1 = new Prompt.Content("text", "What’s in this image?");
    	Prompt.Content contentObj2 = new Prompt.Content("image_url", new Prompt.FileUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"));
    	List<Prompt.Content> contentList = new ArrayList<>();
    	contentList.add(contentObj1);
    	contentList.add(contentObj2);
    	
    	Prompt.Message messageObj = new Prompt.Message("user", contentList);

    	
    	List<Prompt.Message> messages = new ArrayList<>();
    	messages.add(messageObj);

    	
    	// Create Input JSON string
    	String jsonStr = null;
    	try {
            Prompt prompt = new Prompt(model, messages, maxTokens);
        	if(debug) {
        		System.out.println(prompt);
        		System.out.println("\n\n");
        	}
        	jsonStr = JsonConverter.toJson(prompt);
    	}
    	catch (Exception e) {
        	e.printStackTrace();
    	}
    	    	
    	try {
        	URL url = new URL(endpoint);
        	HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        	connection.setRequestMethod("POST");
        	connection.setRequestProperty("Content-Type", "application/json");
        	connection.setRequestProperty("Authorization", "Bearer " + openAIKey);
        	connection.setDoOutput(true);
        	
        	if(debug) {
                System.out.println("Request Headers:");
                connection.getRequestProperties().forEach((key, value) -> System.out.println(key + ": " + value));
        	}
        	
        	OutputStream outputStream = connection.getOutputStream();
        	outputStream.write(jsonStr.getBytes());
        	outputStream.flush();
        	outputStream.close();
        	
        	if(debug) {
                System.out.println("Request Body:\n");
                System.out.println(jsonStr);
        	}

        	int responseCode = connection.getResponseCode();
        	if (responseCode == HttpURLConnection.HTTP_OK) {
            	BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            	StringBuilder response = new StringBuilder();
            	String line;
            	while ((line = reader.readLine()) != null) {
                	response.append(line);
            	}
            	reader.close();

        		System.out.println(JsonConverter.toJson(response.toString()));
        	}
        	else {
            	System.out.println("Error: " + responseCode);
        	}
        	connection.disconnect();
    	}
    	catch (Exception e) {
        	e.printStackTrace();
    	}
	}
}

Welcome to the community!

Thanks for taking the time to format your code!

It’s hard to say, looking at that, but what does “jsonStr” actually look like?

From experience, I would say that it’s probably a backslash issue or something. If printing jsonStr doesn’t yield any insights, it might be a good idea to send it against a mock server to figure out what you’re actually transmitting :thinking:

Error 400 is invalid request and since you mention that the cURL works, this is likely because of the incorrect message content json structure being sent within the call.

I asked Chatty to fix it for you since the correctly working cURL is already included:

After reviewing your Java code, the issue that’s likely causing the HTTP 400 error is related to how the payload is structured, particularly in the way image URLs are represented in the JSON body. The expected JSON structure for the content field in your payload differs from what your Java code is generating, due to the nested structure of the FileUrl class within the Content class.

Here’s the corrected approach:

  1. Correct JSON Structure: Ensure the JSON structure matches exactly what the OpenAI API expects. For an image input, the image_url field should directly contain the URL string, not an object with a url field. However, in your original code, Content class is designed to include a FileUrl object for the image_url, which results in an extra layer in the JSON structure that the API does not expect.

  2. Simplified Approach: Simplify the Content class to handle both text and image URL types without nesting another class for the URL. You can directly include the URL as a String.

Here’s a revised version of the Content class to fix the issue:

/**
 * Inner Class Content
 *
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Content {
    @JsonProperty("type")
    public String type;

    @JsonProperty("text")
    public String text;

    @JsonProperty("image_url")
    public String imageUrl; // Directly use a String for the URL

    // Constructor for text content
    public Content(String type, String text) {
        this.type = type;
        this.text = text;
        this.imageUrl = null;
    }

    // Constructor for image URL
    public Content(String type, String imageUrl) {
        this.type = type;
        this.imageUrl = imageUrl; // Directly assign the URL string
        this.text = null;
    }

    @Override
    public String toString() {
        return JsonConverter.toJson(this);
    }
}

Make sure you adjust the instantiation of Content objects for images accordingly:

// Adjust how you instantiate Content objects for images
Prompt.Content contentObj2 = new Prompt.Content("image_url", "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg");

This adjustment ensures that the JSON payload generated by your Java code matches the expected format by the OpenAI API, eliminating the 400 error caused by structural discrepancies.

Remember to test the modified code to ensure it functions as expected. This approach simplifies handling different types of content and aligns with the JSON structure required by the API.

1 Like