OskarD90
OskarD90

Reputation: 633

Retrofit request body: Required fields

The process of using a request body is described in the official API Declaration page as such:

@POST("users/new")
Call<User> createUser(@Body User user);

While there is no guide for creating the User object, I imagine it can look something like this:

public class User {
    public String name;
    public String group;
}

By extension, this would result in a request body like this:

{ 
    "name": string, 
    "group": string 
}

By default, these fields seem to be optional. What is the best way I can make them required?

Upvotes: 0

Views: 3564

Answers (1)

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21105

There are many ways of accomplishing such a behavior. You can:

  • ... validate your objects to be POSTed before you invoke a Retrofitted-service (user input forms, etc), and let it fail fast.
  • ... validate your domain objects, centralized, in a Retrofit request converter and use chained converters
  • ... validate your data transfer objects objects (if you have any), centralized, after they are converted from domain objects and prepared to be sent
  • ... rely on the server API implementation and don't care for validation at the client side: no need to duplicate server logic ad-hoc, you may run out sync with the server API validation, you write more code, etc. This is what I was suggesting you in that comment.

If you really need to validate the request bodies before they are sent, you should go with the first option. If you want to make the validation fully centralized, you can implement a custom Retrofit converter to make pre-validation on fly. (The code below uses Java 8 and a little bit of Google Guava, Retrofit 2 and Gson, however it can be easily reworked for another components).

Consider these:

interface IService {

    @POST("/")
    Call<String> post(
            @Body User user
    );

}
final class User {

    final String name;
    final String group;

    User(final String name, final String group) {
        this.name = name;
        this.group = group;
    }

}

Now we can implement Retrofit-stuff. The following mockOkHttpClient is a mock OkHttpClient to consume any request and respond with HTTP 200 OK and "OK".

private static final OkHttpClient mockOkHttpClient = new OkHttpClient.Builder()
        .addInterceptor(chain -> new Response.Builder()
                .request(chain.request())
                .protocol(HTTP_1_0)
                .code(HTTP_OK)
                .body(ResponseBody.create(MediaType.parse("application/json"), "\"OK\""))
                .build()
        )
        .build();

Now let's make a simple test:

final Iterable<Retrofit> retrofits = ImmutableList.of(
        getAsIsRetrofit(),
        getValidatedDomainObjectsRetrofit(),
        getValidatedDataTransferObjectsRetrofit()
);
final User user = new User("user", "group");
for ( final Retrofit retrofit : retrofits ) {
    final IService service = retrofit.create(IService.class);
    final String message = service.post(user).execute().body();
    System.out.println(message);
}

As you can see, there are three Retrofit instances that are instantiated with different configurations to demonstrate each of them.

The following Retrofit instance does not care the validation itself. And this is another time I recommend you to go with: simply post what you get as is and let the server API implementation deal with it itself. Consider the API implementation to return nice responses like HTTP 400 Bad Request, etc.

private static Retrofit getAsIsRetrofit() {
    return new Retrofit.Builder()
            .client(mockOkHttpClient)
            .baseUrl("http://whatever")
            .addConverterFactory(GsonConverterFactory.create())
            .build();
}

The following Retrofit instance validates the given User object before it's converted to a Gson-friendly representation (depends on if you have domain objects to data transfer object transformations in your application):

private static Retrofit getValidatedDomainObjectsRetrofit() {
    return new Retrofit.Builder()
            .client(mockOkHttpClient)
            .baseUrl("http://whatever")
            .addConverterFactory(new Converter.Factory() {
                @Override
                public Converter<?, RequestBody> requestBodyConverter(final Type type, final Annotation[] parameterAnnotations,
                        final Annotation[] methodAnnotations, final Retrofit retrofit) {
                    if ( type != User.class ) {
                        return null;
                    }
                    final Converter<Object, RequestBody> nextConverter = retrofit.nextRequestBodyConverter(this, type, parameterAnnotations, methodAnnotations);
                    return (Converter<Object, RequestBody>) value -> {
                        if ( value instanceof User ) {
                            final User user = (User) value;
                            requireNonNull(user.name, "name must not be null");
                            requireNonNull(user.group, "group must not be null");
                        }
                        return nextConverter.convert(value);
                    };
                }

            })
            .addConverterFactory(GsonConverterFactory.create())
            .build();
}

And the next one validates data transfer objects before they are written to output streams. Probably the most low-level instance here.

private static Retrofit getValidatedDataTransferObjectsRetrofit() {
    final Gson gson = new GsonBuilder()
            .registerTypeAdapterFactory(new TypeAdapterFactory() {
                @Override
                public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
                    if ( typeToken.getRawType() != User.class ) {
                        return null;
                    }
                    final TypeAdapter<T> delegateTypeAdapter = gson.getDelegateAdapter(this, typeToken);
                    return new TypeAdapter<T>() {
                        @Override
                        public void write(final JsonWriter out, final T value)
                                throws IOException {
                            if ( value instanceof User ) {
                                final User user = (User) value;
                                requireNonNull(user.name, "name must not be null");
                                requireNonNull(user.group, "group must not be null");
                            }
                            delegateTypeAdapter.write(out, value);
                        }

                        @Override
                        public T read(final JsonReader in)
                                throws IOException {
                            return delegateTypeAdapter.read(in);
                        }
                    };
                }
            })
            .create();
    return new Retrofit.Builder()
            .client(mockOkHttpClient)
            .baseUrl("http://whatever")
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build();
}

Note that requireNonNull is a JDK 8 method, and if you want something like @NotNull, you can implement your own annotation processor, or find such an implementation in the Internet considering my implementation idea useless. :) However, I think you'd like the as-is approach the most.

Upvotes: 3

Related Questions