Serialization, Deserialization & Clean
Coding Principles – With Rest Assured
(Java)
Prepared for: Prabhu Dhandapani | Role: Java Automation Testing Lead | Focus: Rest Assured
+ Clean Code
Part 1: Serialization & Deserialization (Rest Assured + Jackson)
Definition:
• Serialization: Converting Java objects into JSON (or XML) for outbound HTTP requests.
• Deserialization: Converting JSON (or XML) responses into Java objects for type-safe
assertions.
Why it matters (lead-level):
• Type-safety, compile-time checks, cleaner assertions, schema evolution safety with
@JsonIgnoreProperties.
• Enables builders/test data factories instead of ad-hoc Maps/Strings, reducing duplication
and bugs.
POJO with Jackson annotations
package models;
import [Link];
import [Link];
import [Link];
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude([Link].NON_NULL)
public class Employee {
@JsonProperty("id")
private Integer id;
@JsonProperty("name")
private String name;
@JsonProperty("role")
private String role;
@JsonProperty("email")
private String email;
public Employee() {}
public Employee(String name, String role) { [Link] = name; [Link]
= role; }
public Integer getId() { return id; }
public void setId(Integer id) { [Link] = id; }
public String getName() { return name; }
public void setName(String name) { [Link] = name; }
public String getRole() { return role; }
public void setRole(String role) { [Link] = role; }
public String getEmail() { return email; }
public void setEmail(String email) { [Link] = email; }
Serialization – POST body from POJO
Employee emp = new Employee("Prabhu-Auto", "QA Lead");
[Link]("[Link]@[Link]");
Response res = given()
.baseUri([Link]("baseUri", "[Link]
.contentType([Link])
.body(emp)
.when()
.post("/employees")
.then()
.statusCode(201)
.extract().response();
Deserialization – Response to POJO
Employee created = [Link]([Link]);
assertNotNull([Link]());
assertEquals("QA Lead", [Link]());
Nested & List deserialization (jsonPath)
List<Employee> employees = given()
.baseUri(baseUri)
.when()
.get("/employees")
.then()
.statusCode(200)
.extract().jsonPath().getList("items", [Link]);
assertTrue([Link]() > 0);
Real-time scenarios (Serialization/Deserialization)
1) Contract evolves with new optional fields: keep tests stable with @JsonIgnoreProperties
and builders.
2) Negative model case: Deserialize error payload into ErrorResponse POJO and assert
errorCode/message/correlationId.
3) Large payloads: Stream JSON where possible; avoid building massive strings; use POJOs
and Jackson for efficiency.
Part 2: Clean Coding Principles (SOLID + DRY + Simplicity) for API
Automation
Single Responsibility Principle (SRP)
Definition:
A class or method should have only one reason to change.
Why it matters:
Reduces coupling and cognitive load. Easier to maintain and test.
Code example:
// BAD: mixes config, auth, request building, and tests
class ApiUtil { /* getToken(), buildSpec(), postEmployee(), writeExcel() */
}
// GOOD: separate responsibilities
class ConfigManager { /* reads config/system props */ }
class RequestFactory { /* builds base RequestSpecification */ }
interface AuthStrategy { RequestSpecification apply(RequestSpecification
spec); }
class BearerTokenStrategy implements AuthStrategy { /* adds Authorization
header */ }
class ApiClient { /* get/post/put/delete using strategy + factory */ }
Real-time scenario:
When auth or logging changes, only the relevant class changes; tests remain untouched.
Open/Closed Principle (OCP)
Definition:
Open for extension, closed for modification.
Why it matters:
Add new features (e.g., auth types) without breaking stable code.
Code example:
public interface AuthStrategy { RequestSpecification
apply(RequestSpecification spec); }
public class BasicAuthStrategy implements AuthStrategy { /* preemptive
basic */ }
public class BearerTokenStrategy implements AuthStrategy { /* bearer token
*/ }
// Later add
public class ApiKeyStrategy implements AuthStrategy { /* x-api-key header
*/ }
// ApiClient never changes; we just inject new strategy
Real-time scenario:
A project adds API-Key auth for a partner. Add ApiKeyStrategy and wire via config—no core
refactor.
Liskov Substitution Principle (LSP)
Definition:
Subtypes must be substitutable for their base types.
Why it matters:
Allows polymorphic use of clients/strategies in tests.
Code example:
AuthStrategy strategy = new BearerTokenStrategy();
// Later swap
strategy = new BasicAuthStrategy();
ApiClient client = new ApiClient(strategy); // works without changing tests
Real-time scenario:
Switch auth per environment (QA uses basic, UAT uses bearer) without changing test code.
Interface Segregation Principle (ISP)
Definition:
Prefer many small, client-specific interfaces over one large interface.
Why it matters:
Prevents implementing unused methods; clearer contracts.
Code example:
interface GetOperation { Response get(String path); }
interface PostOperation { Response post(String path, Object body); }
// Clients implement only what they need
Real-time scenario:
A read-only service implements only GetOperation; write endpoints use PostOperation—no
empty stubs.
Dependency Inversion Principle (DIP)
Definition:
High-level modules depend on abstractions, not concrete classes.
Why it matters:
Swap implementations (real/mocked) without changing dependents. Improves testability.
Code example:
public class Tests {
private final ApiClient api;
public Tests(ApiClient api){ [Link] = api; }
// In unit tests: inject a FakeApiClient; in E2E: inject RestAssured-backed
ApiClient
Real-time scenario:
Local runs use a mock/fake; CI runs hit QA env. Same tests, different wiring.
DRY (Don't Repeat Yourself)
Definition:
Avoid duplicating logic and configuration.
Why it matters:
Centralized changes, fewer defects.
Code example:
class RequestFactory {
static RequestSpecification base(){
return new RequestSpecBuilder()
.setBaseUri([Link]("baseUri"))
.setContentType([Link])
.log([Link])
.build();
// Reuse [Link]() across all tests
Real-time scenario:
Base URI, headers, and logging are changed in one place; hundreds of tests pick it
automatically.
Reduction of Complexity
Definition:
Prefer small, readable methods; early returns; minimal branching.
Why it matters:
Improves readability and reduces bugs.
Code example:
public Response findEmployee(int id){
return given().spec([Link]())
.pathParam("id", id)
.when().get("/employees/{id}")
.then().extract().response();
Real-time scenario:
Debugging failures is faster when each method has a clear, single purpose and minimal
branching.
Elimination of Duplicate Code
Definition:
Abstract common patterns into helpers/utilities.
Why it matters:
Consistency and fewer maintenance points.
Code example:
public final class ResponseAssert {
public static void assertCreated(Response r){
assertEquals(201, [Link]());
assertNotNull([Link]().get("id"));
// Reuse in all create tests
Real-time scenario:
Standardized checks for create/update/delete reduce test verbosity and mistakes.
Grouping by Functionality
Definition:
Organize packages by feature/domain (users, orders) rather than only by layer.
Why it matters:
Feature ownership and quicker navigation.
Code example:
/employee
[Link]
[Link]
[Link]
[Link]
Real-time scenario:
When a feature changes, you open one folder and see client, tests, schema, and data
together.
Code Reusability
Definition:
Design components (builders, clients, assertions) to be reused across tests/projects.
Why it matters:
Accelerates new suites and keeps quality consistent.
Code example:
public class Retry { /* executeWithRetry(Supplier<Response> ...) */ }
public class TokenProvider { /* get/refresh token */ }
public class JsonUtil { /* toJson/fromJson helpers */ }
Real-time scenario:
New microservice onboarding uses the same core utilities and patterns—only endpoints
and models differ.
Part 3: Interview Q&A (EPAM Style) – Serialization/Deserialization + Clean
Code
Q: What is the advantage of POJOs over Maps for API payloads?
A: POJOs give type-safety, IDE support, and safer evolution with Jackson annotations; Maps
are quick but brittle and error-prone.
Q: How do you keep tests stable when API adds new fields?
A: Use @JsonIgnoreProperties(ignoreUnknown = true) and avoid strict field-by-field equals
unless needed; prefer schema validation for structure.
Q: How do you validate both contract and business rules?
A: Use JSON Schema for contract (shape/types) and AssertJ/Hamcrest for business
fields/values. Separate concerns keeps tests clean.
Q: Explain a time you reduced duplication in API tests.
A: Centralized base RequestSpecification and common assertions; reduced 300+ lines
across suites and made logging consistent.
Q: How do you introduce a new auth mechanism without refactoring tests?
A: OCP + DIP: add a new AuthStrategy implementation and inject via config; ApiClient and
tests stay unchanged.
Q: How do you handle flaky 5xx and 429 in a clean way?
A: Create a reusable Retry utility with exponential backoff, use idempotency keys for POSTs,
and isolate flaky endpoints into a quarantine group.
Q: How do you ensure readability of tests for non-engineering stakeholders?
A: Use BDD (Cucumber) for high-level scenarios and keep step defs thin; push HTTP details
into clients/builders.
Q: How do you structure features in the repo?
A: Group by functionality (employee/, order/) with client, tests, schema, and data together
for quick changes and ownership.
Part 3 (Expanded): EPAM-Style Interview Q&A –
Serialization/Deserialization + Clean Code
Below are 24 additional scenario-driven Q&A (total Part 3 items: 32). They go deeper into
Jackson configuration, polymorphic types, dates, enums, error handling, builders, retry,
token refresh, contract testing, and code quality practices.
Q1: How do you handle nulls during serialization to avoid sending empty fields?
A: Use Jackson @JsonInclude([Link].NON_NULL) at class or ObjectMapper
level to exclude nulls and reduce payload noise.
Q2: Your API returns extra fields not in your POJO. How to keep tests stable?
A: Annotate POJO with @JsonIgnoreProperties(ignoreUnknown = true). This allows
forward-compatible deserialization while you evolve models.
Q3: When would you prefer Maps over POJOs for request bodies?
A: For quick spike tests or dynamic ad-hoc payloads. For maintainable suites, prefer
POJOs/builders for type-safety and reuse.
Q4: How to serialize dates consistently (e.g., ISO-8601) so server accepts them?
A: Configure ObjectMapper with JavaTimeModule and write dates as ISO strings, or ensure
string fields carry RFC3339/ISO format from builders.
Q5: Enum fields failing due to case differences—what’s your approach?
A: Use @JsonProperty on enum constants or a custom deserializer to map input values case-
insensitively; validate against allowed values list.
Q6: Polymorphic JSON (type field decides subclass). How to deserialize?
A: Use @JsonTypeInfo(use = [Link], property = "type") and @JsonSubTypes on the base
class. Keep tests per subtype to validate business rules.
Q7: How do you deserialize only a part of a large response for performance?
A: Use JsonPath to extract just the needed fragment or create a slim POJO that models only
the required subset.
Q8: How do you validate that serialization respects API contract?
A: Combine schema validation with a golden-sample payload built via POJO. Serialize the
POJO and validate it against the JSON schema before sending.
Q9: Object contains computed/derived fields you do not want to send—what to do?
A: Mark with @JsonIgnore on getters/fields or build separate request/response DTOs to
keep concerns clean.
Q10: Your response contains BigDecimal money values—precision issues?
A: Use BigDecimal in POJOs, not double. Assert using string comparison or compareTo(0)
for ranges to avoid floating precision errors.
Q11: The API returns gzip/compressed responses—any change to deserialization?
A: Rest Assured handles it transparently via content-encoding. Ensure Accept-Encoding is
set (usually default) and assert decompressed body fields.
Q12: How do you map snake_case JSON to camelCase POJO fields?
A: Configure ObjectMapper with PropertyNamingStrategies.SNAKE_CASE or annotate with
@JsonProperty to map differing names.
Q13: What’s your strategy for error responses?
A: Create ErrorResponse POJO (code, message, traceId, details[]). On non-2xx, deserialize
into ErrorResponse and assert consistent error contract.
Q14: How do you avoid repeating serialization boilerplate in tests?
A: Centralize in DataFactory/Builder classes that return ready-to-send POJOs; keep
variations as builder methods (withEmail, withRole, invalidEmail).
Q15: Explain a clean approach to token refresh impacting serialization.
A: Keep token logic in TokenProvider; RequestFactory/AuthStrategy uses it. Serialization of
bodies stays independent, respecting SRP.
Q16: How do you trace serialization problems quickly?
A: Log the outbound JSON (with secrets redacted) and validate against schema. Add Allure
attachments with pretty-printed payloads.
Q17: When do you use custom serializers/deserializers?
A: When external format differs (e.g., epoch millis, custom money type). Implement
JsonSerializer/JsonDeserializer and register with ObjectMapper.
Q18: How to assert that deserialization didn’t lose data?
A: Round-trip test: deserialize -> serialize back -> compare significant fields or validate
against schema; ensure enums/dates preserved.
Q19: Handling partial updates (PATCH) using POJOs?
A: Use builders producing minimal POJOs containing only changed fields; ensure nulls are
excluded via @JsonInclude to avoid unintended overwrites.
Q20: What if the same resource has different shapes in v1 vs v2?
A: Version POJOs (EmployeeV1, EmployeeV2) or keep one with optional fields and feature
flags. Run contract validation per version.
Q21: How do you keep serialization logic reusable across services?
A: Publish common DTOs/util libraries (internal Maven artifact) and share
JsonUtil/ObjectMapper configuration via a core module.
Q22: Testing locale-specific formats (numbers/dates)?
A: Send/expect ISO formats in APIs; for locale-sensitive fields, include Accept-Language
headers and assert formatting where business requires.
Q23: How do you guard against breaking changes introduced by refactoring POJOs?
A: Write unit tests for (de)serialization using sample JSON fixtures; run contract/schema
validation in CI on every PR.
Q24: You need to compare responses from two environments for drift—best way?
A: Deserialize both to POJOs, ignore volatile fields (timestamps/ids), and compare using
AssertJ’s usingRecursiveComparison with field filters.
Code Mini-Snippets (for expanded Q&A)
Exclude nulls while serializing:
ObjectMapper mapper = new ObjectMapper()
.setSerializationInclusion([Link].NON_NULL);
Snake_case → camelCase mapping:
ObjectMapper mapper = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
Register JavaTimeModule for dates:
ObjectMapper mapper = new ObjectMapper();
[Link](new JavaTimeModule());
[Link](SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
Custom enum mapping:
enum Status {
@JsonProperty("in_progress") IN_PROGRESS,
@JsonProperty("done") DONE;
Polymorphic type setup:
@JsonTypeInfo(use = [Link], property = "type")
@JsonSubTypes({ @Type(value = [Link], name = "admin"),
@Type(value = [Link], name = "user") })
abstract class User { }
AssertJ recursive comparison ignoring fields:
assertThat(actual).usingRecursiveComparison()
.ignoringFields("id", "timestamp")
.isEqualTo(expected);